I am brand new to VueJS and almost everything is working, except for pagination. As a matter of fact, I have zero warnings. The only thing appearing in the console is "[HMR] Waiting for update signal from WDS... log.js?4244:23" followed by a ">" on the next line.
With that said, the pagination is showing the correct number of pages - given the data coming from the JSON, but I do not know how to connect the pagination to the UL or the app.
At least when there is an error, I can figure something out. Any help is appreciated and thanks in advance.
<template>
<div class="container" id="app">
<span>VueJS-Example</span>
<ul class="list-group list-inline mHeaders">
<li class="list-group-item">Title</li>
<li class="list-group-item">Band</li>
<li class="list-group-item">Date Posted</li>
<li class="list-group-item">Downloads</li>
<li class="list-group-item">YouTube</li>
<li class="list-group-item">MP3</li>
</ul>
<ul :key="item.id" class="list-group list-inline" v-for="item in items">
<li class="list-group-item">
{{item.title}}
</li>
<li class="list-group-item">
{{item.original_band}}
</li>
<li class="list-group-item">
{{item.date_posted}}
</li>
<li class="list-group-item mZip">
<a v-bind:href="''+item.download_midi_tabs+''" target="_blank"></a>
</li>
<li class="list-group-item mYt">
<a v-bind:href="''+item.youtube_link+''" target="_blank"></a>
</li>
<li class="list-group-item mAudio">
<a v-bind:href="''+item.download_guitar_m4v+''" target="_blank"></a>
</li>
</ul>
<pagination :records="288" :per-page="30" #paginate="getPostsViaREST"></pagination>
</div>
</template>
<script>
import axios from 'axios'
import {Pagination} from 'vue-pagination-2'
export default {
name: 'App',
data: function () {
return {
items: [{
title: '',
original_band: '',
date_posted: '',
download_midi_tabs: '',
youtube_link: '',
download_guitar_m4v: ''
}]
}
},
created: function () {
this.getPostsViaREST()
},
methods: {
getPostsViaREST: function () {
axios.get('http://local.sites/getSongs.php')
.then(response => { this.items = response.data })
}
},
components: {
Pagination
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
a {
color: #999;
}
.current {
color: red;
}
ul {
padding: 0;
list-style-type: none;
}
li {
display: inline;
margin: 5px 5px;
}
ul.list-group:after {
clear: both;
display: block;
content: "";
}
.list-group-item {
float: left;
}
.list-group li{
max-width: 30%;
min-width: 50px;
min-height: 48px;
max-height: 48px;
}
.list-group li:first-child{
width: 200px;
cursor: pointer;
}
.list-group li:nth-child(2){
width: 200px;
}
.list-group li:nth-child(3){
width: 110px;
}
.list-group li:nth-child(4){
width: 48px;
}
.list-group li:nth-child(5){
width: 48px;
}
.list-group li:last-child{
width: 48px;
}
.mZip{
background: url("http://www.kronusproductions.com/songs_angular/assets/images/mZip.png");
display: block !important;
max-width: 48px;
height: 48px;
cursor: pointer;
}
.mYt{
background: url("http://www.kronusproductions.com/songs_angular/assets/images/youtube-icon_48x48.png");
display: block !important;
width: 48px;
height: 48px;
cursor: pointer;
}
.mAudio{
background: url("http://www.kronusproductions.com/songs_angular/assets/images/volume.png");
display: block !important;
width: 48px;
height: 48px;
cursor: pointer;
}
.mZip a{
display: block !important;
width: 48px;
height: 48px;
}
.mYt a{
display: block !important;
width: 48px;
height: 48px;
}
.mAudio a{
display: block !important;
width: 48px;
height: 48px;
}
.mHeaders li{
background-color: cornflowerblue;
font-size: 0.85rem;
color: white;
}
OK - this is somewhat of a hack, but it works.
The following URL is a working example:
Needed help from some old fashion native JavaScript on the index.html file. First, I needed to be able to read a hidden field that contained the number of entries coming from the JSON., followed by setting all the ULs to display none - the setTimeout is to make sure that the JSON file was loaded
<script type="text/javascript">
var mTO = setTimeout(function () {
for (var x = 31; x <= $(".numRows").val() - 1; x++) {
if (typeof (document.getElementById('sp_' + x)) === 'undefined') {
} else {
document.getElementById('sp_' + x).style.display = 'none'
}
}
}, 1000);
mTO;
</script>
This is followed by calling a computed user created method to set this.mVar to the number of elements/entries/properties - whatever name you want to use for it - for Vue to know how many elements, so that we could divide by the number of pages to paginate
computed: {
mFunc: function () {
this.mVar = Object.keys(this.items).length
return Object.keys(this.items).length
}
}
This is another portion of the aforementioned hack - depending on page clicked in the pagination section, this determines what to hide and what to show
setPage: function (page) {
this.page = page
// console.log(page)
for (var y = 0; y <= this.mVar - 1; y++) {
if (typeof (document.getElementById('sp_' + y)) === 'undefined') {
} else {
document.getElementById('sp_' + y).style.display = 'none'
}
}
for (var x = (30 * (this.page - 1)); x <= 30 * (this.page); x++) {
if (typeof (document.getElementById('sp_' + x)) === 'undefined' || (document.getElementById('sp_' + x)) == null) {
break
} else {
document.getElementById('sp_' + x).style.display = 'block'
}
}
}
In case you were wondering what the "30" is for, that is the number of ULs that I wanted to show per page.
The last portion of the hack is within the template section
<pagination :records="mFunc" :per-page="30" #paginate="setPage"></pagination>
<span style="display: none;"><input type="hidden" class="numRows" :value="this.mVar" /></span>
If you would like to use the entire, then you can find it on my github:
Github repo for vuejs-example
Related
I am attempting to conditionally render a and based on whether or not a user is signed in or not using AWS amplify and Vue 3 for the frontend. I have been able to get it to not render and then render on sign in but when I log back out the navbar elements are still there and should have disappeared. I am new to Vue, so this maybe an easy fix but am unsure. I have tried making using both computed and a watch to try and force update the computed but that is not working. Any help would be much appreciated. The code is below:
<template>
<header>
<nav class="navbar">
<router-link #click="closeMenu" to="/" class="nav-branding"
>Portal</router-link
>
<ul :class="[menuIsOpen ? 'active' : '', 'nav-menu']" v-show="isSignedIn">
<li #click="toggleMenu" class="nav-item">
<router-link to="/pensions" class="nav-link">Pensions</router-link>
</li>
<li #click="toggleMenu" class="nav-item">
<router-link to="/benefits" class="nav-link">Benefits</router-link>
</li>
<li #click="toggleMenu" class="nav-item">
<router-link to="/annual-leave" class="nav-link"
>Annual Leave</router-link
>
</li>
<li #click="signOut" class="nav-item">
<router-link to="/" class="nav-link">Sign Out</router-link>
</li>
</ul>
<div
#click="toggleMenu"
:class="[menuIsOpen ? 'active' : '', 'hamburger']"
v-show="isSignedIn"
>
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</div>
</nav>
</header>
<div :class="[menuIsOpen ? 'pushed' : 'static']"></div>
</template>
<script>
import { Auth } from "aws-amplify";
export default {
name: "NavBar",
data() {
return {
menuIsOpen: false,
};
},
methods: {
toggleMenu() {
this.menuIsOpen = !this.menuIsOpen;
},
closeMenu() {
this.menuIsOpen = false;
},
async signOut() {
try {
await Auth.signOut();
// navigate to the login page or another route
this.$router.push("/");
} catch (err) {
console.log(err);
}
},
async isUser() {
try {
await Auth.currentAuthenticatedUser();
return true;
} catch {
return false;
}
},
},
computed: {
isSignedIn() {
return this.isUser();
},
watch: {
isSignedIn() {
this.$forceUpdate();
},
},
},
};
</script>
<style>
header {
height: auto;
background-color: #0d1520;
}
li {
list-style: none;
}
a {
color: white;
text-decoration: none;
}
.navbar {
min-height: 70px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
}
.nav-menu {
display: flex;
justify-content: space-around;
align-items: center;
gap: 60px;
}
.nav-branding {
font-size: 2rem;
color: #03e9f4;
}
.nav-branding:hover {
text-shadow: 0 0 5px #03e9f4, 0 0 25px #03e9f4, 0 0 50px #03e9f4,
0 0 100px #03e9f4;
}
.nav-link {
transition: 0.7s ease;
color: #03e9f4;
}
.nav-link:hover {
text-shadow: 0 0 5px #03e9f4, 0 0 25px #03e9f4, 0 0 50px #03e9f4,
0 0 100px #03e9f4;
}
.hamburger {
display: none;
cursor: pointer;
}
.bar {
display: block;
width: 25px;
height: 3px;
margin: 5px auto;
transition: all 0.3s ease-in-out;
background-color: #03e9f4;
}
.hamburger:hover .bar {
box-shadow: 0 0 5px #03e9f4, 0 0 25px #03e9f4, 0 0 50px #03e9f4,
0 0 100px #03e9f4;
}
#media (max-width: 768px) {
.static {
transition: all 0.5s ease-in-out;
padding-top: 0;
z-index: 0;
background-color: #151f31;
}
.pushed {
padding-top: 168px;
transition: padding 0.3s ease-in-out;
transition-delay: 0.2;
z-index: 0;
background-color: #151f31;
}
.hamburger {
display: block;
}
.hamburger.active .bar:nth-child(2) {
opacity: 0;
}
.hamburger.active .bar:nth-child(1) {
transform: translateY(8px) rotate(45deg);
}
.hamburger.active .bar:nth-child(3) {
transform: translateY(-8px) rotate(-45deg);
}
.nav-menu {
position: fixed;
left: 100%;
top: 70px;
gap: 0;
flex-direction: column;
background-color: #0d1520;
width: 100%;
text-align: center;
transition: 0.5s;
}
.nav-item {
margin: 16px 0;
}
.nav-menu.active {
z-index: 1;
left: 0;
}
}
</style>
Update
There're way too many "unknowns" in your problem thus it's difficult to give you a working answer. I'll give you hints but please read the Vue docs carefully to understand how to implement them and to also better understand the framework.
As you mentioned in the comments below, you're handling your sign in function in a "sibling" component and that both components are imported in the App.vue.
App.vue
views/
... NavBar.vue
... Login.vue
Option #1
Without the use of a state management, what you can do is move this code:
export default {
data() {
return { isSignedIn: false };
},
methods: {
async isAuthenticated() {
try {
await Auth.currentAuthenticatedUser();
this.isSignedIn = true;
} catch {
this.isSignedIn = false;
}
},
}
async mounted() {
await this.isAuthenticated();
}
}
in your App.vue, then use an emitter in the login component so that your App.vue can "listen" whenever the user has logged in successfully. You can use this.isAuthenticated() as the callback function in the emit event prop. Then, pass the this.isSignedIn state as a prop in your navbar component:
Login.vue
export default {
...
methods: {
signInUser() {
/** your sign in logic */
this.$emit('signIn')
}
}
...
}
App.vue
<!-- Sample template -->
<NavBar :show="isSignedIn" />
<Login #sign-in="isAuthenticated" />
Navbar.vue
export default {
props: ['show'] // you can then pass show in your v-show
}
Option #2
You can also conditionally render the entire navbar component. However, you need to re-structure your templates a bit:
App.vue
<Navbar v-show="isSignedIn"/>
Option #3
Ideally, App should not be responsible for managing the isSignedIn as the only components that use this state are the NavBar and Login components. With that said, consider using Pinia to manage the states between components.
I am trying to create a paginated flexbox using data from an API but struggle with it although I read the setup step-by step.
Here is my code without styles:
<template>
<div id="app">
<paginate name="articles" :list="articles" class="paginate-list">
<li v-for="item in paginated('articles')">
{{ item }}
</li>
</paginate>
<paginate-links for="items" :show-step-links="true"></paginate-links>
<paginate-links for="items" :limit="2" :show-step-links="true">
</paginate-links>
<paginate-links for="items" :simple="{ next: 'Next »', prev: '« Back' }">
</paginate-links>
</div>
</template>
<script>
import axios from 'axios';
import VuePaginate from 'vue-paginate'
export default {
data() {
return {
items:[],
paginate: [articles]
}
},
created() {
axios.get(https://zbeta2.mykuwaitnet.net/backend/en/api/v2/media-center/press-release/?page_size=61&type=5)
.then(response => {
this.items = response.data
})
}
}
</script>
Step 1: Install npm install --save vue-paginate
Step 2: Import vue-paginate component in main.js
import VuePaginate from "vue-paginate";
Vue.use(VuePaginate);
Step 3: HTML template will be like,
<template>
<div id="app">
<paginate ref="paginator" class="flex-container" name="items" :list="items">
<li
v-for="(item, index) in paginated('items')"
:key="index"
class="flex-item">
<h4>{{ item.pub_date }}, {{ item.title }}</h4>
<img :src="item.image && item.image.file" />
<div class="downloads">
<span
v-for="downloadable in item.downloadable.filter(
(d) => !!d.document_en
)"
:key="downloadable.id">
<a :href="downloadable.document_en.file">Download</a>
</span>
</div>
</li>
</paginate>
<paginate-links
for="items"
:limit="2"
:show-step-links="true"></paginate-links>
</div>
</template>
Step 4: Your component script like
<script>
import axios from "axios";
export default {
data() {
return {
items: [],
paginate: ["items"],
};
},
created() {
this.loadPressRelease();
},
methods: {
loadPressRelease() {
axios.get(`https://zbeta2.mykuwaitnet.net/backend/en/api/v2/media-center/press-release/?page_size=61&type=5`)
.then((response) => {
this.items = response.data.results;
});
}
}
};
</script>
Step 5: CSS style
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
ul.flex-container {
padding: 0;
margin: 0;
list-style-type: none;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-flex-flow: row wrap;
flex-direction: row wrap;
flex-wrap: wrap;
justify-content: space-around;
}
li img {
display: initial;
height: 100px;
}
.flex-item {
background: tomato;
width: calc(100% / 3.5);
padding: 5px;
height: auto;
margin-top: 10px;
color: white;
font-weight: bold;
text-align: center;
}
.downloads {
margin-top: 10px;
}
ul.paginate-links.items li {
display: inline-block;
margin: 5px;
}
ul.paginate-links.items a {
cursor: pointer;
}
ul.paginate-links.items li.active a {
font-weight: bold;
}
ul.paginate-links.items li.next:before {
content: " | ";
margin-right: 13px;
color: #ddd;
}
ul.paginate-links.items li.disabled a {
color: #ccc;
cursor: no-drop;
}
</style>
DEMO Link
Im trying to implement a moveable text that can also be click and call the method. However when i try to put #click its not calling the function. Can anyone help me? Thank you..
App.vue
<template>
<div id="app">
<div>
<a #click="next()">
<movable class="testmove" posTop="222" posLeft="222" shiftKey="true"
><span>Shift Key Behavior</span></movable
>
</a>
</div>
</div>
</template>
<script>
import movable from "v-movable";
export default {
name: "app",
movable,
methmethods: {
next() {
alert("Hello");
},
},
};
</script>
<style>
body {
padding: 0;
margin: 0;
font-family: Helvetica, Arial;
}
.movable {
cursor: pointer;
}
.testmove {
/* display:block;
position: absolute; */
top: 0;
height: 150px;
width: 150px;
background: #333;
color: white;
}
.modaltitle {
background: blue;
display: block;
width: 100%;
color: white;
}
</style>
Here is the code:
https://codesandbox.io/s/cold-darkness-0zm03?file=/src/App.vue
There is a typo in your code methmethods which should be corrected to methods
You can use start event for the v-movable.
#start: fires immediately after the pointerdown event on the element
I have a List Detail View using Nuxt dynamic routes in SSR mode
The list with overflow-y appears on left and on clicking an item, the contents appear on right
When I click each item, the URL params change and a new item is shown on right
When I press back button the previous item is shown but the scrollbar does not change
How do I make the scrollbar go to the previous item?
Here is the GIF showing the problem, on clicking back, the list should scroll to the previous item
<template>
<div class="root">
<div class="left">
<ul>
<li v-for="i in sortedArticles" :key="i.feedItemId">
<nuxt-link :to="'/articles/' + i.feedItemId" :id="i.feedItemId" no-prefetch>{{ i.title }}</nuxt-link>
</li>
</ul>
</div>
<div class="right">Article {{ $route.params.id }}</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
key(route) {
return 'articles'
},
computed: {
...mapGetters({
sortedArticles: 'news/SORTED_ARTICLES'
})
},
}
</script>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
line-height: 1.8;
}
.root {
display: flex;
flex-direction: row;
height: 100vh;
}
.left {
flex: 1;
overflow-y: auto;
}
.left ul {
list-style-type: none;
}
.left li a {
display: block;
text-decoration: none;
padding: 0.5rem 1rem;
border-bottom: 1px solid #eee;
color: black;
}
.left li a:hover {
color: darkorange;
}
.right {
flex: 1;
}
</style>
The above code is the pages/articles/_id.vue file
How do I achieve this in NUXT
You must be use watch:
<li :ref="'el' + i.feedItemId" v-for="i in sortedArticles" :key="i.feedItemId">
watch: {
'$route.params.id': function(id) {
this.$refs['el' + id].scrollIntoView()
}
}
I'm learning how to use vue.js to pull movie information and display it.
Inside the main application mc I have a method getMovie which isn't being called when another method updateMovie is called. The updateMovie method is called from the event 'switch' which is emitted by a child component details-card
I can see that the title on my application gets changed and the method updateMovie is working if you click on a director and then a movie under that director. The input field also changes value. Why won't the getMovie work?
var mc = new Vue({
el: "#movie-card",
data: {
title: '',
valid: false,
loading: false,
error: false,
mc: {},
directorArray: [],
actorArray: [],
},
computed: {
checkMulti: function(item) {
if (Object.keys(item).length <= 1) {
return false;
} else {
return true;
}
}
},
methods: {
getMovie: function() {
this.cm = {};
console.log("getMovie called")
if (this.title !== "") {
this.loading = true;
this.error = false;
searchString = 'https://www.omdbapi.com/?t=' + this.title;
var that = this;
axios.get(searchString)
.then(function(res) {
that.cm = res.data;
that.directorArray = res.data.Director.trim().split(/\s*,\s*/);
that.actorArray = res.data.Actors.trim().split(/\s*,\s*/);
that.valid = true;
that.loading = false;
})
.catch(function(error) {
console.log(error.message);
that.loading = false;
that.error = true;
})
}
},
updateMovie: function(movie) {
console.log(movie);
this.title = movie;
this.getMovie;
}
}
})
Vue.component('details-card', {
props: [
'name',
'title'
],
template: `
<div>
<a href=""
#click="handleShowDetails($event)"
>
{{ name }}
</a>
<div v-if="visible" class="detailsCard">
<h3 class="removeTopMargin">{{ name }}</h3>
<img :src="picUrl">
<h4>Known for</h4>
<p v-for="movie in knownForArray">
<a href=""
#click="switchMovie(movie.original_title, $event)"
>{{ movie.original_title }}</a>
</p>
</div>
</div>
`,
data: function() {
return {
picUrl: "",
knownForArray: [],
visible: false
}
},
methods: {
handleShowDetails: function($event) {
this.visible = !this.visible;
this.callPic($event);
},
switchMovie( movie , $event) {
if ($event) $event.preventDefault();
console.log("switching to...", movie);
this.$emit('switch', movie);
},
callPic: function(event) {
if (event) event.preventDefault();
let that = this;
let searchString = "https://api.themoviedb.org/3/search/person?api_key=9b8f2bdd1eaf20c57554e6d25e0823a2&language=en-US&query=" + this.name + "&page=1&include_adult=false";
if (!this.picUrl) { //only load if empty
axios.get(searchString)
.then(function(res) {
let profilePath = res.data.results[0].profile_path;
if (profilePath === null) {
that.picUrl = "http://placehold.it/150x200"
} else {
that.picUrl = 'https://image.tmdb.org/t/p/w150/' + profilePath;
}
that.personPic = profilePath;
that.knownForArray = res.data.results[0].known_for;
}).catch(function(err){
console.log(err);
})
}
},
hideDetails: function () {
this.visible= false;
}
}
})
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
*:focus {
outline: none;
}
ul {
padding-left: 1em;
}
.loading {
margin: auto;
max-width: 450px;
}
.details {
display:block;
margin: 1em auto;
text-align: center;
}
.searchBar {
padding: .5em;
-webkit-box-shadow: 0px 2px 16px 2px rgba(168,168,168,0.45);
-moz-box-shadow: 0px 2px 16px 2px rgba(168,168,168,0.45);
box-shadow: 0px 2px 16px 2px rgba(168,168,168,0.45);
text-align: center;
}
.searchBar input {
padding: .5em;
width: 300px;
font-size: 1em;
border: none;
border-bottom: 1px solid gray;
}
.searchBar button {
padding: .2em;
border: 2px solid gray;
border-radius: 8px;
background: white;
font-size: 1em;
color:#333;
}
img {
display: block;
margin: auto;
width: 150px;
padding-bottom: .33em;
}
.card {
max-width: 500px;
margin: 2em auto;
-webkit-box-shadow: 0px 6px 16px 2px rgba(168,168,168,0.45);
-moz-box-shadow: 0px 6px 16px 2px rgba(168,168,168,0.45);
box-shadow: 0px 6px 16px 2px rgba(168,168,168,0.45);
padding: 2em;
}
.detailsCard {
-webkit-box-shadow: 0px 6px 16px 2px rgba(168,168,168,0.45);
-moz-box-shadow: 0px 6px 16px 2px rgba(168,168,168,0.45);
box-shadow: 0px 6px 16px 2px rgba(168,168,168,0.45);
padding: 1em;
}
/* ======= Typography */
html {font-size: 1em;}
body {
background-color: white;
font-family: roboto;
font-weight: 400;
line-height: 1.45;
color: #333;
}
p {margin-bottom: 1.3em;}
h1, h2, h3, h4 {
margin: 1.414em 0 0.5em;
font-weight: inherit;
line-height: 1.2;
}
h2, h3, h4 {
border-bottom: 1px solid gray;
}
h1 {
margin-top: 0;
font-size: 3.998em;
text-align: center;
}
h2 {font-size: 2.827em;}
h3 {font-size: 1.999em;}
h4 {font-size: 1.414em;}
small, .font_small {font-size: 0.707em;}
.removeTopMargin {
margin-top: .5em;
}
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<div id="movie-card">
<div class="searchBar">
<input type="text" ref="input" v-model="title" placeholder="Enter a movie title here..." v-on:keyup.enter="getMovie">
<button type="submit" #click="getMovie">Search</button>
</div>
<div class="loading" v-if="loading">
<BR><BR>
<h1>Loading...</h1>
</div>
<div class="loading" v-if="error">
<BR><BR>
<h1>Something went wrong!</h1>
</div>
<div class="card" v-if="valid && !loading">
<h1> {{ cm.Title }}</h1>
<div v-if="!(cm.Poster === 'N/A')" class="poster">
<img v-bind:src="cm.Poster">
</div>
<div class="details">
<p>{{ cm.Year + " – " + cm.Rated + " – " + cm.Runtime }}</p>
</div>
<p>{{ cm.Plot }}</p>
<div class="directors" v-if="cm.Director">
<h3 v-if="(directorArray.length > 1)">Director</h3>
<h3 v-else>Director</h3>
<p>
<p v-for="(director, index) in directorArray">
<details-card :name="director" v-on:switch="updateMovie"></details-card>
</p>
</p>
</div>
<div class="actors" v-if="cm.Actors">
<h3 v-if="actorArray.length > 1">Actors</h3>
<h3 v-else>Actor</h3>
<p>
<p v-for="(actor, index) in actorArray">
<details-card :name="actor"></details-card>
</p>
</p>
</div>
<div class="ratings" v-if="cm.Ratings">
<h3>Ratings</h3>
<ul>
<li v-for="rating in cm.Ratings">{{ rating.Source }}: {{ rating.Value }}</li>
</ul>
</div>
</div>
</div>