gatsbyjs / gatsby

The best React-based framework with performance, scalability and security built in.
https://www.gatsbyjs.com
MIT License
55.22k stars 10.32k forks source link

How to populate a React Context File with MDX data and asset files from a GraphQL query? #18404

Closed rchrdnsh closed 4 years ago

rchrdnsh commented 4 years ago

So, I have a working persistent music player on a project I am working on, thanks to this article:

https://upmostly.com/tutorials/how-to-use-the-usecontext-hook-in-react?unapproved=15591&moderation-hash=88f22760754aa9ff30643d45bc4c41eb#comment-15591

And it's just great. But The information for the music is contained in the Context File, like so:

const MusicPlayerContext = React.createContext([{}, () => {}]);

const MusicPlayerProvider = (props) => {

  const [state, setState] = useState({
    audioPlayer: new Audio(),
    tracks: [
      {
        name: 'Baktun',
        artist: 'RYKR',
        file: Baktun,
        artwork: BaktunArtwork
      },
      {
        name: 'Bash',
        artist: 'RYKR',
        file: Bash,
        artwork: BashArtwork
      },
      {
        name: 'Frost',
        artist: 'RYKR',
        file: Frost,
        artwork: FrostArtwork
      },
      {
        name: 'Greyskull',
        artist: 'RYKR',
        file: Greyskull,
        artwork: GreyskullArtwork
      },
      {
        name: 'Sprial Up',
        artist: 'RYKR',
        file: SpiralUp,
        artwork: SpiralUpArtwork
      }  
    ],
    currentTrackIndex: null,
    isPlaying: false,
  })

  return (
    <MusicPlayerContext.Provider value={[state, setState]}>
      {props.children}
    </MusicPlayerContext.Provider>
  )

}

export { MusicPlayerContext, MusicPlayerProvider }

...but rather than manually input all this information into the context file via a JS object, I would rather store the music in folders with an MDX file per song and the artwork and audio file in there as well, and then inject that information into the Context file, I assume via GraphQL.

I don't really know how to go about doing that, however, so any guidance or examples would be great, thank you :-)

I'm currently importing all of the audio and artwork files manually into the Context file and manually inputing the track information as well, which is not optimal and scalable.

universse commented 4 years ago

First you need create the mdx nodes with gatsby-plugin-mdx.

plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `tracks`,
        path: `${__dirname}/src/tracks/`,
      },
    },
    {
      resolve: `gatsby-plugin-mdx`,
      options: {
        ...
      },
    },
  ]

Then you can use useStaticQuery to query for tracks in Provider.

const MusicPlayerProvider = (props) => {
  const tracks = useStaticQuery(graphql`
    query Tracks {
      allMdx {
        edges {
          node {
            ...
          }
        }
      }
    }
  `)

  const [state, setState] = useState({
    audioPlayer: new Audio(),
    currentTrackIndex: null,
    isPlaying: false,
  })

  return (
    <MusicPlayerContext.Provider value={[{ tracks, ...state }, setState]}>
      {props.children}
    </MusicPlayerContext.Provider>
  )

}
rchrdnsh commented 4 years ago

Hi @universse,

So I've added this query, which is the same query that I am using from your other example, to query in the context file:

const MusicPlayerContext = React.createContext([{}, () => {}])

const MusicPlayerProvider = (props) => {

  const tracks = useStaticQuery(graphql`
    query Tracks { 
      allMdx(filter: {fileAbsolutePath: {regex: "/content/music/"}}) {
        totalCount
        edges {
          node {
            fields {
              slug
            }
            frontmatter {
              name
              artist
              genre
              bpm
              artwork {
                childImageSharp {
                    fluid(maxWidth: 1000) {
                      ...GatsbyImageSharpFluid
                    }
                  }
              }
              alt
              description
              release(formatString: "MMMM Do, YYYY")
              audio {
                absolutePath
              }
            }
          }
        }
      }
    }
  `)

  const [state, setState] = useState({
    audioPlayer: new Audio(),
    currentTrackIndex: null,
    isPlaying: false,
  })

  return (
    <MusicPlayerContext.Provider value={[{ tracks, ...state }, setState]}>
      {props.children}
    </MusicPlayerContext.Provider>
  )

}

export { MusicPlayerContext, MusicPlayerProvider }

...but without doing anything different to any other file that is accessing and using this context, I am getting this error in the browser:

Screen Shot 2019-10-13 at 9 49 40 AM

...so I'm not sure what to do to fix this, but I also have a custom hook called useMusicPlayer that is using this context, like so:

const useMusicPlayer = () => {

  const [state, setState] = useContext(MusicPlayerContext)

  // Play a specific track
  function playTrack(index) {
    if (index === state.currentTrackIndex) {
      togglePlay()
    } else {
      state.audioPlayer.pause()
      state.audioPlayer = new Audio(state.tracks[index].file)
      state.audioPlayer.play()
      setState(state => ({ ...state, currentTrackIndex: index, isPlaying: true }))
    }
  }

  // Toggle play or pause
  function togglePlay() {
    if (state.isPlaying) {
      state.audioPlayer.pause()
    } else {
      state.audioPlayer.play()
    }
    setState(state => ({ ...state, isPlaying: !state.isPlaying }))
  }

  // Play the previous track in the tracks array
  function playPreviousTrack() {
    const newIndex = ((state.currentTrackIndex + -1) % state.tracks.length + state.tracks.length) % state.tracks.length
    playTrack(newIndex)
  }

  // Play the next track in the tracks array
  function playNextTrack() {
    const newIndex = (state.currentTrackIndex + 1) % state.tracks.length
    playTrack(newIndex)
  }

  // Get the current time of the currently playing track
  function currentTime() {
    if (state.isPlaying) {
      state.audioPlayer.currentTime()
    } 
  }

  return {
    playTrack,
    togglePlay,
    currentTrackName:
      state.currentTrackIndex !== null && state.tracks[state.currentTrackIndex].name,
    currentTrackArtist:
      state.currentTrackIndex !== null && state.tracks[state.currentTrackIndex].artist,
    currentTrackArtwork:
      state.currentTrackIndex !== null && state.tracks[state.currentTrackIndex].artwork,
    currentTime,
    trackList: state.tracks,
    isPlaying: state.isPlaying,
    playPreviousTrack,
    playNextTrack,
  }

}

export default useMusicPlayer

...which I finally then try to use in a TrackList file, like so:

const TrackList = () => {

  const {
    trackList,
    currentTrackName,
    currentTrackArtist,
    currentTrackArtwork,
    playTrack,
    isPlaying
  } = useMusicPlayer()

  return (
    <>
      {trackList.map((track, index) => (
        <Card>
          {/* <Artwork src={track.frontmatter.artwork} alt="Album Artowrk."/> */}
          <Artwork src={currentTrackArtwork} alt="Album Artwork."/> 
          <Button
            whileHover={{ scale: 1.1 }}
            whileTap={{ scale: 0.9 }}
            onClick={() => playTrack(index)}
          >
            {
              currentTrackName === track.frontmatter.name && isPlaying ?
              <img src={PauseButton} />
              :
              <img src={PlayButton} />
            }
          </Button>
          <Text>
            <h1>{currentTrackName}</h1>
            <h3>{currentTrackArtist}</h3>
          </Text>  
        </Card>
      ))}
    </>
  )

}

....and all of these files worked fine when I was using static info hard coded into the context file, but I am getting that error message from before.

So, I'm thinking that maybe I need to add track or tracks in there somewhere, but not sure where I would do that. Or maybe the issue lies somewhere else, but I am unsure as to what the problem might be...

universse commented 4 years ago

For your TrackList component, you need to map trackList like so

...
trackList.edges.map((track, index) => {
  const { frontmatter } = track.node
})
...

Basically because you are querying using graphql, the data structure of trackList has changed and you haven't updated <TrackList /> to reflect that.

Same for useMusicPlayer hook. For e.g. tracks.edges.length instead of tracks.length, tracks.edges[index].node instead of tracks[index] etc.

Another way is converting the new trackList data structure to match the current one in MusicPlayerProvider. That way you don't need to change other files' code.

Not sure I'm missing anything else but you can try that first.

rchrdnsh commented 4 years ago

hmmmmmm....having a bit of trouble following exactly how to go about changing everything across multiple files. I also think that it would be nice to be able to convert the trackList into what it needs to be to work with what has already been established. SO in my head I am thinking that one way to do that would be to create an array and then iterate through all the graphql data that has been returned from the static query to the keys of an object. Don't really know how to do that, tho. If you have any other ideas as to how I could go about this I am all ears. Trying to figure it out on my end as well.

Thinking along these lines, but have no idea how to go about it:

tracks: [
  // iterate over all of the tracks created via the staticQuery
  {
    name: data.allMdx.edges.node.frontmatter.name,
    artist: data.allMdx.edges.node.frontmatter.artist
  },
  {},
  {}
],

So I guess I don't know how to manipulate the data I get back from the query very well, or even at all. I would think I need to inject info from the query into an array of objects called tracks but I'm not sure how to even start doing that.

universse commented 4 years ago

You can do that in MusicPlayerProvider

  const tracks = useStaticQuery(graphql`
    ...
  `)

  // need useMemo to avoid re-computation when state change
  const trackList = useMemo(
    () =>
      tracks.allMdx.edges.map(track => {
        const { frontmatter } = track.node
        const { name, artist } = frontmatter

        return { name, artist }
      }),
    [tracks]
  )
rchrdnsh commented 4 years ago

well, after all that help from you most of this is working! Thank you! XD

....except for a few remaining things...

for example, the album artwork shows up in the TrackList as expected if i use gatsby-image, which makes sense, but I can't get it to show up as the current track artwork in the MusicPlayer.

This is how I am creating the currentAlbumArtwork in the useMusicPlayer hook:

currentTrackArtwork:
  state.currentTrackIndex !== null && state.tracks[state.currentTrackIndex].artwork,

...but I'm thinking that since it is an image using gatsby-image childImageSharp I need to do something different here, I just don't know what needs to be changed. Since this is outside of JSX I'm not sure how to alter it to work properly.

But the biggest and last hurdle I am facing is getting the audio files to actually play. I don't know if I am querying for them right or how to get them to work in the same way that they were working outside of Graphql. I am querying for audio in my frontmatter, which is what I have labeled the field in the mdx file, like so:

---
name: Bash
artist: RYKR
genre: Electro
bpm: 124 bpm
artwork: bash.jpg
alt: Bash artwork.
audio: bash.mp3
description: What streamers and confetti sound like if they have souls.
release: 2019-02-01
---

...and the audio file is in the same folder as the mdx file, and the artwork in working using this approach as well, so I'm not sure what the difference is.

I'm querying the absolutePath of the audio, which is one of the options in the explorer, like so:

audio {
  absolutePath
}

...but i don't know if this is actually getting the audio file, rather than the audio files location. Trying to understand how to work with audio and video files in graphql and gatsby and mdx, but still not clear on this.

This is the last piece to getting this working, so hopefully you might have some insight into how to do this, or where to look to figure out how.

universse commented 4 years ago

For that you need to query for path to audio + image source in the public folder.

// useMusicPlayer.js

const [state, setState] = useContext(MusicPlayerContext)

// query all mp3 and png files from /content/music/
const assets = useStaticQuery(graphql`
  query Assets {
    allFile(filter: {extension: {in: ["mp3", "jpg"]}, absolutePath: {regex: "/content/music/"}}) {
      edges {
        node {
          publicURL
          relativePath
        }
      }
    }
  }
`)

// convert to obj for fast lookup
const assetObj= useMemo(
  () =>
    assets.allFile.edges.reduce((obj, file) => {
      const { publicURL, relativePath } = file.node

      obj[relativePath] = publicURL

      return obj
    }, {}),
  [assets]
)

const artwork = state.tracks[state.currentTrackIndex].artwork  // bash.jpg
const currentTrackArtworkSrc = assetObj[artwork] // /static/bash-[some-hash].jpg

const audio = state.tracks[state.currentTrackIndex].audio // bash.mp3
const currentAudioSrc = assetObj[audio] // /static/bash-[some-hash].mp3

And you don't need to query absolutePath on audio, just audio is enough.

rchrdnsh commented 4 years ago

Hi @universse, apologies for not replying sooner, as work has been rough this past week.

So, I threw this into the app, and the part that trips the app up is this part:

const artwork = state.tracks[state.currentTrackIndex].artwork  // bash.jpg
const currentTrackArtworkSrc = assetObj[artwork] // /static/bash-[some-hash].jpg

const audio = state.tracks[state.currentTrackIndex].audio // bash.mp3
const currentAudioSrc = assetObj[audio] // /static/bash-[some-hash].mp3

...which gives me the following error:

Screen Shot 2019-10-21 at 11 19 12 PM

...so, i guess i don't understand where to use these new consts in the app...I tired this:

    currentTrackArtwork:
      state.currentTrackIndex !== null && currentTrackArtworkSrc,

...but that didn't seem to make any difference. How would creating something like currentTrackArtworkSrc be used in the rest of the files? What should it replace, if anything?

universse commented 4 years ago

So in useMusicPlayer you are having this

return {
  playTrack,
  togglePlay,
  currentTrackName:
    state.currentTrackIndex !== null && state.tracks[state.currentTrackIndex].name,
  currentTrackArtist:
    state.currentTrackIndex !== null && state.tracks[state.currentTrackIndex].artist,
  currentTrackArtwork:
    state.currentTrackIndex !== null && state.tracks[state.currentTrackIndex].artwork,
  currentTime,
  trackList: state.tracks,
  isPlaying: state.isPlaying,
  playPreviousTrack,
  playNextTrack,
}

Now it becomes

const assets = useStaticQuery(graphql`
  query Assets {
    allFile(filter: {extension: {in: ["mp3", "jpg"]}, absolutePath: {regex: "/content/music/"}}) {
      edges {
        node {
          publicURL
          relativePath
        }
      }
    }
  }
`)

// convert to obj for fast lookup
const assetObj= useMemo(
  () =>
    assets.allFile.edges.reduce((obj, file) => {
      const { publicURL, relativePath } = file.node

      obj[relativePath] = publicURL

      return obj
    }, {}),
  [assets]
)

return {
  playTrack,
  togglePlay,
  currentTrackName:
    state.currentTrackIndex && state.tracks[state.currentTrackIndex].name,
  currentTrackArtist:
    state.currentTrackIndex && state.tracks[state.currentTrackIndex].artist,
  currentTrackArtwork:
    state.currentTrackIndex && assetObj[state.tracks[state.currentTrackIndex].artwork],
  currentTrackAudio:
    state.currentTrackIndex && assetObj[state.tracks[state.currentTrackIndex].audio],
  currentTime,
  trackList: state.tracks,
  isPlaying: state.isPlaying,
  playPreviousTrack,
  playNextTrack,
}
rchrdnsh commented 4 years ago

hmmmm...still having the same issue:

Screen Shot 2019-10-22 at 8 11 35 AM

...and we're still not using the constants that we created here:

  const artwork = state.tracks[state.currentTrackIndex].artwork  // bash.jpg
  const currentTrackArtworkSrc = assetObj[artwork] // /static/bash-[some-hash].jpg

  const audio = state.tracks[state.currentTrackIndex].audio // bash.mp3
  const currentAudioSrc = assetObj[audio] // /static/bash-[some-hash].mp3

...so what are the currentTrackArtworkSrc and currentAudioSrc for? Should we not use them somewhere? I tried this but it did not work either:

currentTrackArtwork:
  state.currentTrackIndex
  &&
  currentTrackArtworkSrc[state.tracks[state.currentTrackIndex].artwork],
currentTrackAudio:
  state.currentTrackIndex
  &&
  currentAudioSrc[state.tracks[state.currentTrackIndex].audio],
universse commented 4 years ago

Would you mind sharing your repo so I can take a look later?

rchrdnsh commented 4 years ago

But, of course, thank you XD

https://github.com/rchrdnsh/RYKR

universse commented 4 years ago

First, in the content/music folder, guess you can safely delete artwork and audio folder.

Here's the final code with some comments. I cleaned up the code a bit also.

// MusicPlayerContext
import React, { useState, useMemo } from 'react'
import { useStaticQuery, graphql } from 'gatsby'

const MusicPlayerContext = React.createContext([{}, () => {}])

const MusicPlayerProvider = props => {
  const tracks = useStaticQuery(graphql`
    query Tracks {
      allMdx(filter: { fileAbsolutePath: { regex: "/content/music/" } }) {
        edges {
          node {
            fields {
              slug
            }
            frontmatter {
              name
              artist
              genre
              bpm
              # ADD BASE HERE
              artwork {
                base
                childImageSharp {
                  fluid(maxWidth: 1000) {
                    ...GatsbyImageSharpFluid
                  }
                }
              }
              alt
              description
              release(formatString: "MMMM Do, YYYY")
              audio {
                absolutePath
                base
              }
            }
          }
        }
      }
    }
  `)

  // need useMemo to avoid re-computation when state change
  const trackList = useMemo(
    () =>
      tracks.allMdx.edges.map(track => {
        const { frontmatter } = track.node
        const {
          name,
          artist,
          genre,
          bpm,
          artwork,
          alt,
          description,
          audio,
        } = frontmatter

        return { name, artist, genre, bpm, artwork, alt, description, audio }
      }),
    [tracks]
  )

  const [state, setState] = useState({
    audioPlayer: new Audio(),
    tracks: trackList,
    currentTrackIndex: null,
    isPlaying: false,
  })

  return (
    <MusicPlayerContext.Provider value={[state, setState]}>
      {props.children}
    </MusicPlayerContext.Provider>
  )
}

export { MusicPlayerContext, MusicPlayerProvider }
// useMusicPlayer.js
import { useContext, useMemo } from 'react'
import { useStaticQuery, graphql } from 'gatsby'
import { MusicPlayerContext } from './MusicPlayerContext'

// frost.mp3 -> frost
function basename(name) {
  return name.slice(0, name.lastIndexOf('.'))
}

const useMusicPlayer = () => {
  const [state, setState] = useContext(MusicPlayerContext)

  // query all mp3 and jpg files from /content/music/
  const assets = useStaticQuery(graphql`
    query Assets {
      allFile(
        filter: {
          extension: { in: ["mp3", "jpg"] }
          absolutePath: { regex: "/content/music/" }
        }
      ) {
        edges {
          node {
            publicURL
            relativePath
          }
        }
      }
    }
  `)
  // convert to obj for fast lookup
  const assetObj = useMemo(
    () =>
      assets.allFile.edges.reduce((obj, file) => {
        const { publicURL, relativePath } = file.node

        obj[relativePath] = publicURL

        return obj
      }, {}),
    [assets]
  )

  // Play a specific track
  function playTrack(index) {
    if (index === state.currentTrackIndex) {
      togglePlay()
    } else {
      state.audioPlayer.pause()

      const base = state.tracks[index].audio.base // frost.mp3
      const baseName = basename(base) // frost

      // new Audio() does not support relative path
      // hence the need for window.location.origin
      const audioPlayer = new Audio(
        `${window.location.origin}${assetObj[`${baseName}/${base}`]}`
      ) // new Audio('http://www.domain.com/static/frost-[hash].mp3')

      audioPlayer.play()
      setState(state => ({
        ...state,
        currentTrackIndex: index,
        isPlaying: true,
        audioPlayer,
      }))
    }
  }

  // Toggle play or pause
  function togglePlay() {
    if (state.isPlaying) {
      state.audioPlayer.pause()
    } else {
      state.audioPlayer.play()
    }
    setState(state => ({ ...state, isPlaying: !state.isPlaying }))
  }

  // Play the previous track in the tracks array
  function playPreviousTrack() {
    const newIndex =
      (((state.currentTrackIndex + -1) % state.tracks.length) +
        state.tracks.length) %
      state.tracks.length
    playTrack(newIndex)
  }

  // Play the next track in the tracks array
  function playNextTrack() {
    const newIndex = (state.currentTrackIndex + 1) % state.tracks.length
    playTrack(newIndex)
  }

  // Get the current time of the currently playing track
  function currentTime() {
    if (state.isPlaying) {
      state.audioPlayer.currentTime()
    }
  }

  let currentTrackArtwork, currentTrackAudio

  if (state.currentTrackIndex !== null) {
    const base = state.tracks[state.currentTrackIndex].audio.base // frost.mp3
    const baseName = basename(base) // frost

    currentTrackArtwork =
      assetObj[
        `${baseName}/${state.tracks[state.currentTrackIndex].artwork.base}`
      ] // assetObj['frost/frost.jpg']
    currentTrackAudio =
      assetObj[
        `${baseName}/${state.tracks[state.currentTrackIndex].audio.base}`
      ] // assetObj['frost/frost.mp3']
  }

  return {
    playTrack,
    togglePlay,
    currentTrackName:
      state.currentTrackIndex !== null &&
      state.tracks[state.currentTrackIndex].name,
    currentTrackArtist:
      state.currentTrackIndex !== null &&
      state.tracks[state.currentTrackIndex].artist,
    currentTrackArtwork,
    currentTrackAudio,
    currentTime,
    trackList: state.tracks,
    isPlaying: state.isPlaying,
    playPreviousTrack,
    playNextTrack,
  }
}

export default useMusicPlayer
rchrdnsh commented 4 years ago

WOW!!! You are amazing! This all works so well now, and I'm starting to understand things better as I go through the code. Things like regex and useMemo and a bunch of other stuff are all new to me, so thank you for showing me these things and how they can be used.

This is truly amazing XD !!!!

rchrdnsh commented 4 years ago

Do you have any way to be paid or tipped or anything? This is truly helpful to me :-)

universse commented 4 years ago

Haha thanks but it's not necessary. Guess you can pay it forward next time :)

rchrdnsh commented 4 years ago

Hi @universse!

I'm trying to finish this music player off, but having trouble...dunno where to go, so i figured I'd ask you, if you don't mind.

Trying to make a progress bar that shows the current time in minutes and seconds on the left, a bar that visualizes the duration of the track that the listener can click on and/or drag on to move around in the track in the middle, and the total duration on the other side. This is very much like every music player that exists, I would imagine. Also want a volume slider, but I'll save that for later.

Anyway, been googling and trying a bunch of stuff out and I got to here:

function getTime(time) {
    if(!isNaN(time)) {
      return Math.floor(time / 60) + ':' + ('0' + Math.floor(time % 60)).slice(-2)
    }
  }

useEffect(() => {
    const currentTime = setInterval(() => {getTime(state.audioPlayer.currentTime)}, 1000)
    return () => clearInterval(currentTime)
  }, []);

...where the first function formats the time into minutes and seconds and the second useEffect hook is trying to update the currentTime every second.

My issue is that I get the currentTime to update, but only when I hit the pause button, rather than updating every second. Some with the duration, which starts off as NaN but the first time I hit pause it shows up.

I feel like I might be close, but not quite there yet, and I can't see what I'm missing or doing wrong.

universse commented 4 years ago

Perhaps you could try this. You need to put currentTime into the component's state and update it.

const [currentTime, setCurrentTime] = useState(state.audioPlayer.currentTime)

useEffect(() => {
  const timeoutId= setInterval(() => {
    setCurrentTime(getTime(state.audioPlayer.currentTime))
  }, 1000)
  return () => clearInterval(timeoutId)
}, [state.audioPlayer]);

Also, I suggest you add https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks to your setup to avoid potential bugs.

rchrdnsh commented 4 years ago

yup! that basically works! There is just a second delay if i click play on a new track for the seconds to update to the new track, which makes sense, i think. I'm thinking maybe a conditional statement that looks for the track index to change, and if so clears out the currentTimes state instantly, or something along those lines...

So i needed to have a second useState then, which i'm thinking i can use to make the progress bar as well. I was not really clear on the idea of useState, but I'm assuming that I can have as many useState's as needed?

I will also grab that hook linter as well, thank you again :-)

universse commented 4 years ago

You can try this also

const [currentTime, setCurrentTime] = useState(state.audioPlayer.currentTime)

// both formattedTime and progress are states derived from audioPlayer.currentTime
// so no need another useState
const formattedTime = getTime(currentTime)
const progress = currentTime / state.audioPlayer.duration

useEffect(() => {
  const timeoutId= setInterval(() => {
    setCurrentTime(state.audioPlayer.currentTime)
  }, 1000)
  return () => {
    // clean up function run when state.audioPlayer changes
    // reset currentTime to 0
    setCurrentTime(0)
    clearInterval(timeoutId)
  }
}, [state.audioPlayer]);
rchrdnsh commented 4 years ago

yeah, that's working a bit better now, except there is still a little bit of lag between clicking a songs play button and having the number reset to '0:00'...maybe i need some default placeholder values or something, but not a big deal...

next up are the sliders, one to show and change the progress of the song, and another to change the volume of the song...doing research now, but if you have any thoughts, or suggestions, I'm all ears :-)

universse commented 4 years ago

For the progress slider, you can use <input type='range' />, something along this line.

<input
  max={state.audioPlayer.duration}
  min='0'
  step='1'
  type='range'
  value={currentTime}
  onChange={e => {
    setCurrentTime(e.target.value)
    state.AudioPlayer.currentTime = e.target.value
  }}
/>

As for volume, you can also use <input type='range' />. Anw, it would be great if you can share your repo, cuz I am not sure how to go about doing it.

rchrdnsh commented 4 years ago

Here's the repo:

https://github.com/rchrdnsh/RYKR

trying to wrap my head around using the state from useMusicPlayer in this example, but it's still a bit confusing for me :-/

rchrdnsh commented 4 years ago

hmmmm...so with your example would i be adding this code to the useMusicPlayer hook file? I don't think I have access to state.AudioPlayer.currentTime outside of it, do I?

Would I make a function and name it something like this?:

function progressSlider() {
  return (
    <input
      max={state.audioPlayer.duration}
      min='0'
      step='1'
      type='range'
      value={currentTime}
      onChange={e => {
        setCurrentTime(e.target.value)
        state.AudioPlayer.currentTime = e.target.value
      }}
    />
  )
}

...then export it from the useMusicPlayer.js file and import it into the PlayerControls.js file?

I am currently doing that now and the slider shows up but does not move with the currentTime, then when I try to click on the slider to change the position of the music I get the following error:

Screen Shot 2019-11-14 at 10 36 31 AM

...which i am thinking it means that i need to set up some state management in the PlayerControls.js file...not sure if that's correct, or how to do it, though...

universse commented 4 years ago

ProgressSlider is a new component. It can access state.audioPlayer.currentTime via useContext.

function ProgressSlider() {
  const [{ audioPlayer }] =  useContext(MusicPlayerContext)

  return (
    <input
      max={audioPlayer.duration}
      min='0'
      step='1'
      type='range'
      value={currentTime}
      onChange={e => {
        setCurrentTime(e.target.value)
        // not AudioPlayer
        state.audioPlayer.currentTime = e.target.value
      }}
    />
  )
}
rchrdnsh commented 4 years ago

hmmmm...so it's kinda working...couple things, though...

  1. Cannot click and drag on the control to slide around the slider.
  2. When I click on another part of the slider the currentTime updates to a strange number before formatting to minutes and seconds.
  3. Can't figure out how to fill in the elapsed time with another color (although apparently this is next to impossible in webkit/safari, so not that important)

Here's a gif showing the number thing, and also not draggable 'thumb', as it seems to be called:

2019-11-18 22 40 35

I hope that makes sense...I' m trying to figure it out myself, but as you know, I am not super good at this kind of stuff...🤷‍♂️

universse commented 4 years ago

Ah I think the strange number is the time in seconds. So I guess you need to format it

<input
  max={audioPlayer.duration}
  min='0'
  step='1'
  type='range'
  value={currentTime} // this should have alr been formatted to minutes and seconds
  onChange={e => {
    setCurrentTime(getTime(e.target.value)) // getTime will format time to minutes and seconds, you probably already have it somewhere
    state.audioPlayer.currentTime = e.target.value
  }}
/>

Try this and see if you can drag the slider.

rchrdnsh commented 4 years ago

I tried doing exactly that earlier, but it did not, and still does not seem to work, neither for the formatting or for the slider control.

The following is also the getTime code as well as the currentTime and duration, among other things...the number does format eventually, but there is a delay of a few milliseconds or so, which is enough to be noticeable to the user(me! XD)...

// Transform the currentTime info into minutes and seconds
  function getTime(time) {
    if(!isNaN(time)) {
      return Math.floor(time / 60) + ':' + ('0' + Math.floor(time % 60)).slice(-2)
    }
  }

  // both formattedTime and progress are states derived from audioPlayer.currentTime
  // so no need another useState
  const formattedTime = getTime(currentTime)
  const progress = currentTime / state.audioPlayer.duration

  useEffect(() => {
    const timeoutId= setInterval(() => {
      setCurrentTime(getTime(state.audioPlayer.currentTime))
      // setCurrentTime(formattedTime)
    }, 1000)
    return () => {
      // clean up function run when state.audioPlayer changes
      // reset currentTime to 0
      setCurrentTime(0)
      clearInterval(timeoutId)
    }
  }, [state.audioPlayer]);

  // get and display the duration of the track, in minutes and seconds
  const duration = getTime(state.audioPlayer.duration)

dunno if you can spot anything off in there...still working on adding a volume control as well :-)

universse commented 4 years ago

I downloaded your code. I will refactor quite a bit. Will need some time.

universse commented 4 years ago

I guess what you seem unclear about is the difference between sharing state vs sharing stateful logic.

What you want is sharing state about the music being played across all components. What your useMusicPlayer hook is doing is sharing stateful logic, which is unnecessary for your app.

// MusicPlayerContext
import React, { useState, useMemo, useContext, useEffect } from 'react'
import { useStaticQuery, graphql } from 'gatsby'

const MusicPlayerContext = React.createContext([{}, () => {}])

const MusicPlayerProvider = props => {
  // COMMENT_ADDED
  // query both tracks and assets since only one staticQuery per file
  const { tracks, assets } = useStaticQuery(graphql`
    query Tracks {
      tracks: allMdx(
        filter: { fileAbsolutePath: { regex: "/content/music/" } }
      ) {
        edges {
          node {
            fields {
              slug
            }
            frontmatter {
              name
              artist
              genre
              bpm
              # ADD BASE HERE
              artwork {
                base
                childImageSharp {
                  fluid(maxWidth: 1000) {
                    ...GatsbyImageSharpFluid
                  }
                }
              }
              alt
              description
              release(formatString: "MMMM Do, YYYY")
              audio {
                absolutePath
                base
              }
            }
          }
        }
      }

      # query all mp3 and jpg files from /content/music/
      assets: allFile(
        filter: {
          extension: { in: ["mp3", "jpg"] }
          absolutePath: { regex: "/content/music/" }
        }
      ) {
        edges {
          node {
            publicURL
            relativePath
          }
        }
      }
    }
  `)

  // need useMemo to avoid re-computation when state change
  const trackList = useMemo(
    () =>
      tracks.edges.map(track => {
        const { frontmatter } = track.node
        const {
          name,
          artist,
          genre,
          bpm,
          artwork,
          alt,
          description,
          audio,
        } = frontmatter

        return { name, artist, genre, bpm, artwork, alt, description, audio }
      }),
    [tracks]
  )

  const [state, setState] = useState({
    audioPlayer: new Audio(),
    // COMMENT_ADDED
    // don't really need trackList in state
    // tracks: trackList,
    currentTrackIndex: null,
    isPlaying: false,
  })

  const [currentTime, setCurrentTime] = useState(state.audioPlayer.currentTime)

  // both formattedTime and progress are states derived from audioPlayer.currentTime
  // so no need another useState
  const formattedTime = getTime(currentTime)
  const progress = currentTime / state.audioPlayer.duration

  // get and display the duration of the track, in minutes and seconds
  const formattedDuration = getTime(state.audioPlayer.duration)

  useEffect(() => {
    // COMMENT_ADDED
    // reset currentTime to 0 when state.audioPlayer changes
    setCurrentTime(0)
  }, [state.audioPlayer])

  useEffect(() => {
    // COMMENT_ADDED
    // if isPlaying, start the timer
    if (state.isPlaying) {
      const timeoutId = setInterval(() => {
        setCurrentTime(currentTime => currentTime + 1)
      }, 1000)

      return () => {
        // COMMENT_ADDED
        // clear interval run when paused i.e. state.isPlaying is false
        clearInterval(timeoutId)
      }
    }
  }, [state.isPlaying])

  // convert to obj for fast lookup
  const assetObj = useMemo(
    () =>
      assets.edges.reduce((obj, file) => {
        const { publicURL, relativePath } = file.node
        obj[relativePath] = publicURL
        return obj
      }, {}),
    [assets]
  )

  function playTrack(index) {
    if (index === state.currentTrackIndex) {
      togglePlay()
    } else {
      state.audioPlayer.pause()

      const base = trackList[index].audio.base // frost.mp3
      const baseName = basename(base) // frost

      // new Audio() does not support relative path
      // hence the need for window.location.origin
      const audioPlayer = new Audio(
        `${window.location.origin}${assetObj[`${baseName}/${base}`]}`
      ) // new Audio('http://www.domain.com/static/frost-[hash].mp3')

      audioPlayer.play()
      setState(state => ({
        ...state,
        currentTrackIndex: index,
        isPlaying: true,
        audioPlayer,
      }))
    }
  }

  // Toggle play or pause
  function togglePlay() {
    if (state.isPlaying) {
      state.audioPlayer.pause()
    } else {
      state.audioPlayer.play()
    }
    setState(state => ({ ...state, isPlaying: !state.isPlaying }))
  }

  // Play the previous track in the tracks array
  function playPreviousTrack() {
    const newIndex =
      (((state.currentTrackIndex + -1) % trackList.length) + trackList.length) %
      trackList.length
    playTrack(newIndex)
  }

  // Play the next track in the tracks array
  function playNextTrack() {
    const newIndex = (state.currentTrackIndex + 1) % trackList.length
    playTrack(newIndex)
  }

  let currentTrackName,
    currentTrackArtist,
    currentTrackArtwork,
    currentTrackAudio

  // COMMENT_ADDED
  // simplify things a bit
  if (state.currentTrackIndex !== null) {
    const { currentTrackIndex } = state
    const currentTrack = trackList[currentTrackIndex]

    const base = currentTrack.audio.base // frost.mp3
    const baseName = basename(base) // frost

    currentTrackName = currentTrack.name
    currentTrackArtist = currentTrack.artist
    currentTrackArtwork = assetObj[`${baseName}/${currentTrack.artwork.base}`] // assetObj['frost/frost.jpg']
    currentTrackAudio = assetObj[`${baseName}/${currentTrack.audio.base}`] // assetObj['frost/frost.mp3']
  }

  return (
    <MusicPlayerContext.Provider
      value={{
        playTrack,
        togglePlay,
        currentTrackName,
        currentTrackArtist,
        currentTrackArtwork,
        currentTrackAudio,
        currentTime,
        // COMMENT_ADDED
        // setCurrentTime to be used by ProgressSlider
        setCurrentTime,
        formattedDuration,
        formattedTime,
        // volume,
        audioPlayer: state.audioPlayer,
        trackList,
        isPlaying: state.isPlaying,
        playPreviousTrack,
        playNextTrack,
      }}
    >
      {props.children}
    </MusicPlayerContext.Provider>
  )
}

// COMMENT_ADDED
// access global state from MusicPlayerContext
function useMusicPlayerState() {
  return useContext(MusicPlayerContext)
}

export { useMusicPlayerState, MusicPlayerProvider }

// frost.mp3 -> frost
function basename(name) {
  return name.slice(0, name.lastIndexOf('.'))
}

// Transform the currentTime info into minutes and seconds
function getTime(time) {
  if (!isNaN(time)) {
    return Math.floor(time / 60) + ':' + ('0' + Math.floor(time % 60)).slice(-2)
  }
}
// TrackList.js
import React from 'react'
import styled from 'styled-components'
import { motion } from 'framer-motion'
import Img from 'gatsby-image'

import { H1, H2, H3 } from '../components/Typography'

import PlayButton from '../images/controls/play-button.svg'
import PauseButton from '../images/controls/pause-button.svg'

import { useMusicPlayerState } from './MusicPlayerContext'

// const TrackGrid = styled.div`
//   margin: 1rem;
//   display: grid;
//   grid-template-rows: auto;
//   grid-template-columns: 1fr 1fr 1fr;
//   grid-gap: 1rem;
//   border: 1px solid red;
// `

const Card = styled.div`
  margin: 0;
  padding: 0;
  text-decoration: none;
  line-height: 1;
  background: black;
  cursor: pointer;
  box-sizing: border-box;
  width: 100%;
  height: auto;
  display: block;
  position: relative;
  /* display: grid;
  grid-template-columns: repeat(8, 1fr);
  grid-template-rows: repeat(16, 1fr); */
  background: #000;
  transition: All 200ms ease;
  z-index: 1;
  /* border: 1px solid white; */
`

const Artwork = styled(Img)`
  position: relative;
  margin: 0;
  padding: 0;
  max-width: 25rem;
  z-index: 1;
  border-radius: 16px;
`

const Text = styled.div`
  position: relative;
  z-index: 1;
  background: #222;
  border-radius: 16px;
  margin: -3rem 1rem 1rem 1rem;
  padding: 1rem;
  box-shadow: 0px 0px 16px rgba(0, 0, 0, 0.75);

  > p {
    font-size: 24px;
  }
`

const Button = styled(motion.button)`
  margin: -6rem 0 0 15rem;
  padding: 1.5rem;
  width: 6rem;
  height: 6rem;
  border: none;
  border-radius: 3rem;
  background: #333;
  position: relative;
  box-shadow: 0px 0px 16px rgba(0, 0, 0, 0.5);
  z-index: 5;

  :focus {
    outline: none;
  }

  :active {
    outline: none;
    box-shadow: 0px 0px 16px white;
    /* background: black; */
  }
`

const TrackList = () => {
  const {
    trackList,
    currentTrackName,
    playTrack,
    isPlaying,
  } = useMusicPlayerState()

  return (
    <>
      {trackList.map((track, index) => (
        // COMMENT_ADDED: add key here
        <Card key={index}>
          <Artwork
            fluid={track.artwork.childImageSharp.fluid}
            alt={track.alt}
          />
          <Button
            whileHover={{ scale: 1.1 }}
            whileTap={{ scale: 0.9 }}
            onClick={() => playTrack(index)}
          >
            {currentTrackName === track.name && isPlaying ? (
              <img src={PauseButton} alt="Pause Button" />
            ) : (
              <img src={PlayButton} alt="PlayButton" />
            )}
          </Button>
          <Text>
            <H1>{track.name}</H1>
            {/* <h3>{track.artist}</h3> */}
            {/* <p>{track.genre}</p> */}
            {/* <p>{track.bpm}</p> */}
          </Text>
        </Card>
      ))}
    </>
  )
}

export default TrackList
// PlayerControls.js
import React, { useState } from 'react'
import styled from 'styled-components'
import { H1, H2, H3 } from '../components/Typography'

import { useMusicPlayerState } from './MusicPlayerContext'

import previousButton from '../images/controls/previous-button.svg'
import playButton from '../images/controls/play-button.svg'
import pauseButton from '../images/controls/pause-button.svg'
import nextButton from '../images/controls/next-button.svg'

const Button = styled.button`
  margin: 1rem;
  padding: 0.8rem;
  width: 3rem;
  height: 3rem;
  border: none;
  background: #333;
  border-radius: 32px;

  :focus {
    outline: none;
    /* box-shadow: 0px 0px 16px white; */
  }

  :active {
    outline: none;
    box-shadow: 0px 0px 16px white;
    background: black;
  }
`

const Image = styled.img`
  margin: 0;
  padding: 0;
  height: 1.5rem;
  width: 1.5rem;
`

const FlexStart = styled.div`
  display: flex;
  align-items: center;
  justify-content: start;
`

const FlexCenter = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
`

const TrackName = styled(H1)`
  margin: 0 1rem;
  padding: 0;
`

const TrackDuration = styled.h3`
  margin: 0;
  padding: 0.5rem;
`

const TrackArtwork = styled.img`
  margin: 0 0.5rem;
  padding: 0;
  width: 4rem;
  height: 4rem;
  border: none;
`
// const ProgressBar = styled.progress`
//   margin: 0 1rem;
//   background-color: #333;
// `

// COMMENT_ADDED
// ProgressSlider is a standalone component
// it accesses global music state with the useMusicPlayerState hook
// jump to anywhere on the track using a progress bar style interface
function ProgressSlider() {
  const {
    audioPlayer,
    currentTime,
    formattedTime,
    setCurrentTime,
  } = useMusicPlayerState()

  return (
    <input
      max={audioPlayer.duration}
      min="0"
      step="1"
      type="range"
      value={currentTime}
      onChange={event => {
        // COMMENT_ADDED
        // convert event.target.value, a string, to number
        const newTiming = parseInt(event.target.value, 10)

        setCurrentTime(newTiming)
        // not AudioPlayer
        audioPlayer.currentTime = newTiming
      }}
      // COMMENT_ADDED
      // onInput is not needed
    />
  )
}

const Controls = () => {
  // const [state, setState] = useContext(MusicPlayerContext)
  // const [currentTime, setCurrentTime] = useState(state.audioPlayer.currentTime)

  const {
    isPlaying,
    currentTrackName,
    currentTrackArtwork,
    currentTime,
    formattedDuration,
    formattedTime,
    volume,
    togglePlay,
    playPreviousTrack,
    playNextTrack,
  } = useMusicPlayerState()

  // const [time, setTime] = useState(currentTime)

  // const handleChange = event => {
  //   setTime(event.target.value)
  // }

  // e => {
  //   setCurrentTime(e.target.value)
  //   state.AudioPlayer.currentTime = e.target.value
  // }

  return (
    <>
      <FlexStart>
        <TrackArtwork src={currentTrackArtwork} alt="Album Artwork." />
        <TrackName>{currentTrackName}</TrackName>
      </FlexStart>
      <FlexCenter>
        <Button onClick={playPreviousTrack} disabled={!currentTrackName}>
          <Image src={previousButton} alt="Previous Button" />
        </Button>
        <Button onClick={togglePlay} disabled={!currentTrackName}>
          {isPlaying ? (
            <Image src={pauseButton} alt="Pause Button" />
          ) : (
            <Image src={playButton} alt="Play Button" />
          )}
        </Button>
        <Button onClick={playNextTrack} disabled={!currentTrackName}>
          <Image src={nextButton} alt="Next Button" />
        </Button>
      </FlexCenter>
      <FlexCenter>
        <TrackDuration>{formattedTime}</TrackDuration>
        <ProgressSlider />
        <TrackDuration>{formattedDuration}</TrackDuration>
      </FlexCenter>
    </>
  )
}

export default Controls

I added some comments as well. Just search for COMMENT_ADDED. If there's any part you are confused about, feel free to clarify.

rchrdnsh commented 4 years ago

hmmmmm....I had not heard of or thought about the difference between state and stateful logic, so thank you very much for that :-)

So, to clarify, state would be the currently playing track and all of its information, and stateful logic would be the music player that manipulates the currently playing track, like the progress bar, that allows the listener to navigate in time within the currently playing track?

I think I'm starting to understand this a bit better. Gonna need some time to go through what you have done, but it looks like absolute magic to my inexperienced eyes. The progress you have helped me achieve is truly amazing and has motivated me so much, and it is much much much appreciated XD

rchrdnsh commented 4 years ago

so I did this to create placeholder text for the formattedDuration:

const formattedDuration = state.isPlaying ? getTime(state.audioPlayer.duration) : '0:00'

...and it generally works, except I'm getting that same delay between updates when the song changes...like so:

2019-11-19 17 33 22

I'm thinking I need a useState but I'm not sure...maybe it can be done in the <ProgressSlider> itself

universse commented 4 years ago

Stateful logic involves how you manipulate that state, what happens when that state changes, those sorts of things. So let say you want to create another music player, you can share those logic with the new player instead of rewriting it from scratch. On the other hand, if you share state between those two music players, they will play the same song etc...

I think state.audioPlayer.duration might be undefined during those delay. So maybe you can do

const formattedDuration = getTime(state.audioPlayer.duration) || '0:00'
rchrdnsh commented 4 years ago

yeah, that totally worked :-) ....also something I have not seen yet, which is the logical OR operator, yes? (just googled it)

Logically speaking this means:

thing 1                          ||                                thing 2

do thing 1...................................but if that is not there...................................then do thing 2

yes?

That is great to know :-)

universse commented 4 years ago

Yep. If thing1 is a falsy value, like undefined, null, 0, or ''.

rchrdnsh commented 4 years ago

Hey @universse, got one more thing I want to try and do, but I'm not sure how to go about it. I have many mdx files that are articles about music and I am putting musical examples in them. There will be a varied amount of examples in each article, but what i want them to do is be included in the audio context so that they play through the persistent player, stopping whatever might be playing. Trying to figure out how to do this, but I am a bit lost. If you have any thoughts or suggestions on where to even begin, I am all ears.

An example would be:

---
title: Music Stuff
category: Theory
---

# Heading

Some words and stuff.

<Audio src="example-audio.mp3"/>

More words and stuff.

Where the inline audio example would play through the persistent music player and be controlled by the player controls as well. I hope that makes sense.

universse commented 4 years ago

Can you share your code for <Audio />?

rchrdnsh commented 4 years ago

I don't have anything yet for the <Audio/> tag.

I tried using a plain <audio> tag in an mdx file, and that didn't work, then I tried using gatsby-remark-audio and that didn't work either, so I'm currently learning the Web Audio API to see if there is anything in there that could help me with this.

I'm also running into bugs in mdx itself, like these:

https://github.com/gatsbyjs/gatsby/issues/19785

https://github.com/gatsbyjs/gatsby/issues/19825

...which are roughly related to this, I think:

https://github.com/gatsbyjs/gatsby/issues/16242

...and I believe @ChristopherBiscardi is working on this issue, but not sure when mdx will be viable again.

I'm thinking maybe wrapping the mdx template file in the audio context and then selecting all the audio tags and adding them to the context somehow? Not sure how to do that yet, though, or if that is even a good idea 🤔

universse commented 4 years ago

I guess first you need a playArticleTrack function in MusicPlayerProvider.js, similar to playTrack function.

  function playArticleTrack(src) {
    state.audioPlayer.pause()

    const audioPlayer = new Audio(src)

    audioPlayer.play()
    setState(state => ({
      ...state,
      currentTrackIndex: -1, // I assume article tracks are not parts of the original track list
      isPlaying: true,
      audioPlayer,
    }))
  }

Then in your Audio component

function Audio ({ src }) {
  const { playArticleTrack } = useMusicPlayerState()

  return ... 
}

And you need to wrap your mdx article with <MusicPlayerProvider />, using https://www.gatsbyjs.org/docs/browser-apis/#wrapPageElement

rchrdnsh commented 4 years ago

yes, the article tracks are not part of the original tracklist...so, I am trying to implement your suggestion, and I have done this so far to the Audio.js component:

import React from 'react'
import { useMusicPlayerState } from '../player/MusicPlayerContext'

function Audio ({ src }) {
  const { playArticleTrack } = useMusicPlayerState()

  return (
    <audio controls onPlay={playArticleTrack} src={src}></audio>
  )
}

export default Audio

...and I'm trying to implement that component in the index page first to try and get it to work, like so:

// ...other imports before these...

import Audio from '../components/Audio'
import Groove from './groove.mp3'

const IndexPage = () => (
  <Container>
    <SEO title="Home" />
    <Audio src={Groove}></Audio>
    <TrackList />
  </Container>
)

and I get the following error message when i hit play on the controls of the

Screen Shot 2019-12-06 at 12 00 21 AM

...so I'm trying to wrap my head around what it means and what I need to do, but if you have any thoughts...

universse commented 4 years ago

Can you try this and see how it goes?

  function playArticleTrack(src) {
    state.audioPlayer.pause()

    const audioPlayer = new Audio(src)

    audioPlayer.play()
    setState(state => ({
      ...state,
      currentTrackIndex: 0, // I change to 0 instead
      isPlaying: true,
      audioPlayer,
    }))
  }

If currentTrackIndex is -1, currentTrack is undefined.

rchrdnsh commented 4 years ago

so I tried that and it sort of worked, except it seems now that whatever track 0 is in the index is replaced by this other audio file, and then the app breaks when I try to play that other track again. It even shows the artwork of the track at index position 0 when I click play on the article Audio track in question...

I'm also thinking that I don't really even need a tracklist at all. Or, that is to say, that what I really want is audio files on many different pages and template pages(inside mdx files, etc...) to all simply play through the global, persistent music player, and to all have metadata and artwork and to all be added to a listening history of some sort.

So maybe making a tracklist is not the correct approach, and instead having an audio component that i can use anywhere on any page that taps into the global music context and then adds itself to the playlist history is more in line with what i am needing for this project...

rchrdnsh commented 4 years ago

hmmmm, so I set the currentTrackIndex to null when the article track plays, and it sort of works, although it keeps playing without sound after the file is done playing. Code looks like this:

function playArticleTrack(src) {
    state.audioPlayer.pause()

    const audioPlayer = new Audio(src)

    audioPlayer.play()
    setState(state => ({
      ...state,
      // currentTrackIndex: 0, // I change to 0 instead
      currentTrackIndex: null, // Trying null and it seems to work, although the player keeps going after it's done.
      isPlaying: true,
      audioPlayer,
    }))
  }

...is this ok to do? Will this cause other issues in the future that I am unaware of?

rchrdnsh commented 4 years ago

also, are you on www.codementor.io or anything, or have any time or interest in helping me finish this project beyond open source help? I realize I am quite in over my head but would love to get this done sooner than later and have a working product to help build my music businesses upon. If not, no worries, just thought I would ask you first before expanding my search :-)

universse commented 4 years ago

Are you open to contacting over Whatsapp/Telegram?

rchrdnsh commented 4 years ago

I haven't used those before, but sure :-)

rchrdnsh commented 4 years ago

before we get to that, though. I'm currently trying to link each track card in the trackList to open an mdx file about that track, but I'm not having much luck.

I'm trying to add a slug constant and then access it in the trackList to link to individual MDX files about each track, like so:

  // need useMemo to avoid re-computation when state change
  const trackList = useMemo(
    () =>
      tracks.edges.map(track => {
        // add slug to the data...
        const slug = track.node.fields.slug
        const { frontmatter } = track.node
        const {
          name,
          artist,
          genre,
          bpm,
          artwork,
          alt,
          description,
          audio,
        } = frontmatter

        // then add slug to the return...
        return slug, { name, artist, genre, bpm, artwork, alt, description, audio }
      }),
    [tracks]
  )

...then use that to create the link to the programmatically created mdx file in the tracklist component, like so:

const TrackList = () => {
  const {
    trackList,
    currentTrackName,
    playTrack,
    isPlaying,
  } = useMusicPlayerState()

  return (
    <>
      {trackList.map((track, index) => (
        // use the track slug to link to the mdx file with the same slug...
        <Card key={index} to={track.slug}>
          <Artwork
            fluid={track.artwork.childImageSharp.fluid}
            alt={track.alt}
          />
          <Button
            whileHover={{ scale: 1.1 }}
            whileTap={{ scale: 0.9 }}
            onClick={() => playTrack(index)}
          >
            {currentTrackName === track.name && isPlaying ? (
              <img src={PauseButton} alt="Pause Button" />
            ) : (
              <img src={PlayButton} alt="Play Button" />
            )
            }
          </Button>
          <Text>
            <H1>{track.name}</H1>
            <H3>{track.artist}</H3>
            {/* <p>{track.genre}</p> */}
            {/* <p>{track.bpm}</p> */}
          </Text>
          <LinkButton>Learn More</LinkButton>
        </Card>
      ))}
    </>
  )
}

export default TrackList

...but this is currently not working at all in any way, and I get an error that the the value of track.slug is not defined in the console:

Screen Shot 2019-12-16 at 2 53 08 PM

So, I'm thinking that I'm not too far off here, but I'm not sure what I'm missing to make it work right, as I don't fully understand what I'm doing :-(

universse commented 4 years ago

Is that a typo?

return { slug, name, artist, genre, bpm, artwork, alt, description, audio }

instead of

return slug, { name, artist, genre, bpm, artwork, alt, description, audio }
rchrdnsh commented 4 years ago

so, i'm a little confused... this looks like an object to me, due to the curly braces:

return { slug, name, artist, genre, bpm, artwork, alt, description, audio }

...but it's returning what looks like an array of data? There are no property: value pairs, just single words, so I thought that maybe it is similar in syntax to ES module imports and exports where you have to name the imports and exports inside of curly braces if they are not the default export, which I also don't fully understand, to be honest. That's why I put the slug outside of the curly braces, as I don't understand why it would be inside or outside of them.

I will try to put it inside and see if that works :-)

universse commented 4 years ago

It is called object shorthand. You can read more here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer

Basically, it is a shortcut. So instead o

const obj = { a: a, b: b, c: c }

You can write

const obj = { a, b, c }