So I have edited this. What I'm trying to do is search through an array of music tracks using v-model. Here is the code in the vue component.
<script setup>
import SongRow from '../components/SongRow.vue';
import Magnify from 'vue-material-design-icons/Magnify.vue';
import tracks from '../tracks.json'
import { useSongStore } from '../stores/song'
import { storeToRefs } from 'pinia';
const useSong = useSongStore()
const {isPlaying, currentTrack,} = storeToRefs(useSong)
const playFunc = () => {
if (currentTrack.value) {
useSong.playOrPauseThisSong(currentTrack.value)
return
}
useSong.playFromFirst()
}
const searchTerm = store.state.searchTerm
const filteredTracks = computed(() =>{
const term = searchTerm.toLowercase()
return store.state.tracks.filter(track =>
tracks.description.toLowercase().includes(term))
})
return {searchTerm, filteredTracks}
</script>
<template>
<div
id="TopNav2"
class="fixed position-absolute top-0 right-0 flex items-center
justify-between w-[calc(100%-240px)] h-[56px] border-b border-b-
[#32323D]"
>
<div class="flex items-center w-full">
<Magnify class="pl-6 mt-1 pr-2" fillColor="#7E7E88"
:size="22"/>
<input
class="
p-1
bg-transparent
outline-none
font-[300]
placeholder-[#BEBEC7]
text-[#FFFFFF]
w-full
max-w-xl
"
placeholder="Double click to search all tracks..."
type="text"
v-model="searchTerm"
>
</div>
</div>
<div class="border-b border-b-[#302d2d]"></div>
<div class="mb-10"></div>
<div id="SongsSection" class="w-[calc(100%-1px)]">
<div class="mb-4"></div>
<div class="flex items-center justify-between min-w-[590px]
mx-8 border-b border-b-[#302d2d] py-2.5 px-1.5">
<div class="text-xs font-light text-
[#aeaeae]">TRACK</div>
</div>
<ul class="px-5 w-[calc(100%-22px)]" v-for="track in
filteredTracks.tracks" :key="track.id">
<SongRow v-if="track" :track="track"/>
</ul>
</div>
<div class="mb-40"></div>
</template>
<style scoped>
.circle {
width: 4px;
height: 4px;
background-color: rgb(189, 189, 189);
border-radius: 100%;
}
</style>
And here is the SongRow Component code:
<template>
<li
@mouseenter="isHover = true"
@mouseleave="isHover = false"
class="display:inline-block p-2 ml-4 hover:bg-[#979797] hover:bg-opacity-5"
>
<div class="display:inline-block">
<div>
<img width="38" height="38" class="absolute p-0 mt-2 border border-[#494949]" :src="track.cover_art_path">
</div>
<div
v-if="isHover"
class="p-1 mt-[11.5px] ml-[3px] absolute rounded-full cursor-pointer text-white drop-shadow-[0_1.2px_1.2px_rgba(0,0,0,0.9)]"
>
<Play
v-if="!isPlaying"
@click="useSong.playOrPauseThisSong(track)"
/>
<Play
v-else-if="isPlaying && currentTrack.name !== track.name"
@click="useSong.loadSong(track)"
/>
</div>
<div
@mouseenter="isHoverGif = true"
@mouseleave="isHoverGif = false"
v-if="isPlaying && track && currentTrack && currentTrack.name === track.name"
class="p-1 mt-[11.5px] ml-[3px] absolute rounded-full cursor-pointer text-white drop-shadow-[0_1.2px_1.2px_rgba(0,0,0,0.9)]"
>
<img
v-if="!isHoverGif"
src="/images/sound-wave.gif"
>
<Pause v-if="isHoverGif" :size="25" @click="useSong.playOrPauseSong()"/>
</div>
<div
v-if="track"
:class="track && currentTrack && currentTrack.name === track.name ? 'text-[#4ea1ff]' : 'text-[#d4d4d4]'"
class="flex items-center text-[13px] ml-12 p-0 pt-1 font-bold"
>
{{ track.name }}
</div>
</div>
<div class="flex items-center justify-between">
<div
v-if="track"
:class="track && currentTrack && currentTrack.description === track.description ? 'text-[#4ea1ff]' : 'text-[#d4d4d4]'"
class="align-left text-[13px] ml-12 p-0 font-[200] text-[#d4d4d4]"
>
{{ track.description }}
</div>
<div class="flex">
<div
v-if="isTrackTime"
:class="track && currentTrack && currentTrack.name === track.name ? 'text-[#EF5464]' : 'text-[#d4d4d4]'"
class=" text-[13px] p-2 font-[200] text-[#d4d4d4]"
>
{{ isTrackTime }}
</div>
<a :href=(track.license_path) target=”_blank” type="button" class=" rounded-full p-2 hover:bg-[#2b2b30]">
<Basket fillColor="#EAEAEA" :size="20"/>
</a>
<div class=" rounded-full p-2 hover:bg-[#979797] hover:bg-opacity-20 cursor-pointer">
<DotsHorizontal fillColor="#CCCCCC" :size="21"/>
</div>
</div>
</div>
</li>
</template>
<script setup>
import { ref, toRefs, onMounted } from 'vue'
import music from '../tracks.json'
import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue';
import Play from 'vue-material-design-icons/Play.vue';
import Pause from 'vue-material-design-icons/Pause.vue';
import Basket from 'vue-material-design-icons/basket.vue';
import { useSongStore } from '../stores/song'
import { storeToRefs } from 'pinia';
const useSong = useSongStore()
const { audio, isPlaying, currentTrack, } = storeToRefs(useSong)
let isHover = ref(false)
let isHoverGif = ref(false)
let isTrackTime = ref('00:00')
const props = defineProps({ track: Object, String })
const { track } = toRefs(props)
onMounted(() => {
const audioMeta = new Audio(track.value.path);
audioMeta.addEventListener('loadedmetadata', () => {
const duration = audioMeta.duration;
const minutes = Math.floor(duration / 60);
const seconds = Math.floor(duration % 60);
isTrackTime.value = minutes+':'+seconds.toString().padStart(2, '0')
});
})
</script>
And here is what is in Pinia:
import { defineStore } from 'pinia'
import tracks from '../tracks'
export const useSongStore = defineStore('song', {
state: () => ({
isPlaying: false,
audio: null,
currentTrack: null,
trackTime: null,
currentVolume: 80,
id:'',
name:'',
description:'',
cover_art_path:'',
license_path:'',
keywords:'',
genre:'',
moods:'',
tempo:'',
theme:'',
}),
actions: {
loadSong(track, id, description, cover_art_path, license_path, keywords, genre, moods, tempo, theme,) {
this.currentTrack = track
this.currentId = id
this.currentDescription = description
this.currentCover_Art_Path = cover_art_path
this.currentLicense_Path = license_path
this.currentKeywords = keywords
this.currentGenre = genre
this.currentMoods = moods
this.currentTempo = tempo
this.currentTheme = theme
if (this.audio && this.audio.src) {
this.audio.pause()
this.isPlaying = false
this.audio.src = ''
}
this.audio = new Audio()
this.audio.src = track.path
setTimeout(() => {
this.isPlaying = true
this.audio.play()
}, 200)
},
playOrPauseSong() {
if (this.audio.paused) {
this.isPlaying = true
this.audio.play()
} else {
this.isPlaying = false
this.audio.pause()
}
},
playOrPauseThisSong(track) {
if (!this.audio || !this.audio.src || (this.currentTrack.id !== track.id)) {
this.loadSong(track)
return
}
this.playOrPauseSong()
},
prevSong(currentTrack) {
let track = tracks.tracks[currentTrack.id - 2]
this.loadSong(track)
},
nextSong(currentTrack) {
if (currentTrack.id === tracks.tracks.length) {
let track = tracks.tracks[0]
this.loadSong(track)
} else {
let track = tracks.tracks[currentTrack.id]
this.loadSong(track)
}
},
playFromFirst() {
this.resetState()
let track = tracks.tracks[0]
this.loadSong(track)
},
resetState() {
this.isPlaying = false
this.audio = null
this.currentTrack = null
}
},
persist: true
})
If I remove all the search filter code it works perfectly as a full array but as soon as I add the filter it breaks so I'm clearly doing it wrong.
I'm quite new to this and am just a musician trying to create my own website so something that might be obvious to you guys is not so obvious to me. Any help would be great and hugely appreciated.
Here are the issues that I noticed. It might not be comprehensive as it's a lot of code to parse through (I didn't really look much through the Pinia store or SongRow component, I don't think most of their code matters as much in relation to your issue):
store
:e.g. const searchTerm = store.state.searchTerm
I'm not sure what store
is in reference to. It's not being imported from anywhere and your Pinia store is already imported as useSongStore
(and assigned to useSong
). I might be missing some context regarding store
, but regardless based on the usage of searchTerm it looks like it should work just fine as a normal ref variable
const searchTerm = ref('');
Unless you have auto importing that you didn't mention you'll need to import computed
(and the ref
above) from vue:
<script setup>
import { ref, computed } from 'vue';
...
return {searchTerm, filteredTracks}
This line can be removed. It looks like you've mixed up the syntax of Composition API using setup() function which does use a return statement, and the syntax of Composition API using script setup which does not use a return statement.
filteredTracks
computed functionWith my advice that searchTerm
be made a ref, some code here will need to be changed. Also, another reference to store
here when I think you mean to use the useSong
store object... However I noticed tracks
is not a piece of state on your Pinia store (again, I'm probably missing some context on what store
is). I actually think instead of importing tracks.json into your store and your component that tracks.json should be added to your store's state.
stores/song.js
import tracks from './tracks';
export const useSongStore = defineStore('song', {
state: () => ({
tracks: tracks,
...
Now the computed function can search tracks just using the store and you no longer need to separately import ../tracks.json
const filteredTracks = computed(() => {
// added if statement to return all songs while searchTerm is empty string
if (searchTerm.value === '') return useSong.tracks;
// additional fix for typo "toLowercase" should be "toLowerCase" (uppercase C)
const term = searchTerm.value.toLowerCase();
return useSong.tracks.filter(track =>
track.description.toLowerCase().includes(term)
);
});
v-for
looping on filteredTracks.tracks
This is an error because filteredTracks
is itself the array of tracks already, filtered from the useSong.tracks
array, so the .tracks
part here can be dropped.
<ul class="px-5 w-[calc(100%-22px)]" v-for="track in filteredTracks" :key="track.id">
<SongRow v-if="track" :track="track" />
</ul>