In this tutorial we'll be learning to upload files to S3 and records to Dynamo using AWS Amplify.

In the previous article we had our app playing songs straight from S3.  To build upon that we first need to create a way to add a song record. We start by creating an Add button.

{
    showAddSong ? <AddSong /> : <IconButton> <AddIcon /> </IconButton>
}

We then also need to add the AddIcon to our imports.

import AddIcon from '@material-ui/icons/Add';

Now we can move onto creating the new AddSong component. We can create this at the bottom of our App.jsx file.

const AddSong = () => {
    return (
        <div className="newSong">
            <TextField label="Title" />
            <TextField label="Artist" />
            <TextField label="Description" />
        </div>
    )
}

We also need to add TextField to our imports from material UI.

import { Paper, IconButton, TextField } from '@material-ui/core';

The next thing to do is add the ability to open our new component by controlling the showAddSong variable. We need to create a new state declaration next to the others.

const [showAddSong, setShowAddNewSong] = useState(false);

We can now update our new AddIcon button to set showAddSong to true.

<IconButton onClick={() => setShowAddNewSong(true)}>
    <AddIcon />
</IconButton>

To change it back, we can add a parameter to our AddSong component called onUpload. When this gets called we will reset the showAddSong to false.

<AddSong
    onUpload={() => {
        setShowAddNewSong(false);
    }}
/>

We then need to update our component to work with that new parameter and a button to "upload" the new song. That button calls a function in the component where we will add the ability to upload the data, but for now we will just call the onUpload function.

const AddSong = ({ onUpload }) => {
    const uploadSong = async () => {
        //Upload the song
        onUpload();
    };

    return (
        <div className="newSong">
            <TextField
                label="Title"
            />
            <TextField
                label="Artist"
            />
            <TextField
                label="Description"
            />
            <IconButton onClick={uploadSong}>
                <PublishIcon />
            </IconButton>
        </div>
    );
};

And now we add the PublishIcon to our imports and we're ready to test this out.

import PublishIcon from '@material-ui/icons/Publish';

When we start up the app and log in we now get a plus icon, clicking that we can enter some details for the song and click upload.

Updating AddSong

Now we want to be able to store and access the data that a user enters into the fields when adding a song.

const AddSong = ({ onUpload }) => {
    const [songData, setSongData] = useState({});

    const uploadSong = async () => {
        //Upload the song
        console.log('songData', songData);
        const { title, description, owner } = songData;

        onUpload();
    };

    return (
        <div className="newSong">
            <TextField
                label="Title"
                value={songData.title}
                onChange={e => setSongData({ ...songData, title: e.target.value })}
            />
            <TextField
                label="Artist"
                value={songData.owner}
                onChange={e => setSongData({ ...songData, owner: e.target.value })}
            />
            <TextField
                label="Description"
                value={songData.description}
                onChange={e => setSongData({ ...songData, description: e.target.value })}
            />
            <IconButton onClick={uploadSong}>
                <PublishIcon />
            </IconButton>
        </div>
    );
};

We've also had to change all of the TextFields to be controlled, passing in a value from out state and providing an onChange too. If we save this and try entering some details before uploading we should see a console.log of the details in our chrome console.

Next we need to add the ability to actually upload the song. For this we'll be using the default html input with a type of file. Add this to the JSX just before the upload icon button.

<input type="file" accept="audio/mp3" onChange={e => setMp3Data(e.target.files[0])} />

As you may have noticed we are calling setMp3Data on change. This is some more state in the AddSong component.

const [mp3Data, setMp3Data] = useState();

Now we have all of the data that we need, we can start by uploading the song to S3 and then the data to our database. To upload the song we're going to use the Amplify Storage class again. The fileName is going to be a UUID so we also need to run npm install --save uuid in our terminal and then import it at the top of our file import { v4 as uuid } from 'uuid';. We then pass in the mp3Data and a contentType and we get back an object with a key.

const { key } = await Storage.put(`${uuid()}.mp3`, mp3Data, { contentType: 'audio/mp3' });

Now we have the key we can create the record for the song in the database. As there may be multiple songs with the same name, we'll use an UUID as the ID again.

const createSongInput = {
    id: uuid(),
    title,
    description,
    owner,
    filePath: key,
    like: 0,
};
await API.graphql(graphqlOperation(createSong, { input: createSongInput }));

To get this to work we need to import the createSong mutator that was created when we created the dynamo storage with Amplify.

import { updateSong, createSong } from './graphql/mutations';

The last thing that we need to do is to make the app re-get the data from the database once we've finished uploading it. We can do this by adding a fetchSongs call as part of the onUpload function.

<AddSong
    onUpload={() => {
        setShowAddNewSong(false);
        fetchSongs();
    }}
/>

Now when we reload the page, we can click to add a new song, input the details, select our new song, upload it and then play it back from the app