Build A Transcription App with Strapi, ChatGPT, & Whisper: Part 2

Michael - Sep 7 - - Dev Community

Introduction

In the first part of this series, we introduced the project's concept, explained the technologies we will use, coded the portion of the frontend that uses Whisper to transcribe recorded audio, and then coded the relevant UI. In this part of the series, we will incorporate Strapi CMS to save data with our own custom API and then show how to hook this up to the frontend code.

Outline

You can find the outline for this series below:

How to Install Strapi

Let's create a folder containing the source code for our Strapi CMS API. First, open a terminal and navigate to the root directory transcribe-tutorial, and run the command below:

npx create-strapi-app@latest strapi-transcribe-api
Enter fullscreen mode Exit fullscreen mode

We should receive a confirmation once this is complete and it finishes installing the dependencies.

It should automatically direct us to the Strapi dashboard as it starts our project. We must create our administrator here, so fill out the form and click the "Let's start" button.

We should now see the dashboard, which looks like this:

welcome on board to Strapi.png

Create Strapi Collection Types and Fields

We will need to create data structures to save data to the Content management system (CMS).

Navigate to the dashboard in your browser, and on the left-hand side menu, select Content-Type Builder, which should bring you to the below screen:

create collection type in strapi.png

Click on Create new collection type to open a form where you can specify the name and type of the data you wish to store.

Perhaps a word on how the app will be displayed before we describe the shape of the data. It will display chunks of transcribed text alongside an analysis or description of said parts, and we will also display an overview of the meeting at the top. Here is the data structure:

 

const meeting = {
  title: "",
  overview: "",
  ended: false,
  transcribedChunks: [
    {
      text: "",
      analysis: "",
      answer: ""
    },
    {
      text: "",
      analysis: "",
      answer: ""
    }
  ]
};

Enter fullscreen mode Exit fullscreen mode

Create Strapi Collections for Meetings and Transcriptions

This means we will create two collections: meetings and transcribed chunks. One meeting will have many transcribed chunks, requiring us to set up a one-to-many relationship between these two collections.

Our data structure also has the field ended, allowing us to lock the meeting for further transcriptions once it has finished.

First, let's create the transcribed-chunks collection by typing transcribed-chunk into the display name field and clicking continue:

create transcribed chunk collection.png

Create all three of the fields mentioned above:

  • text
  • analysis
  • answer with a type of Long text:

create collection field.png

Then click finish; at the top right-hand corner, click save. Clicking this will trigger a server restart; just wait for that to finish.

Now, we will create our meeting collection and the relationship between it and the transcribed chunks. Click on create new collection type again, type meeting for the name of our collection, and then click continue.

create meeting collection.png

Create the following fields:
a text field for the

  • title (Short Text)
  • overview (Long Text)
  • ended (relation:meeting has many transcribed-chunks):

create relationship in strapi.png

Then click Finish and save in the top right-hand corner. Now that we have the CMS set up, we can save meetings with an overview generated by AI. We also have a boolean field named ended to indicate that the meeting has finished and that it is a record of what was transcribed. Each meeting will have an array of many transcribed chunks that will contain the transcribed text and a mix of answers or analyses depending on what the user has chosen to generate.

Enable API Public Access

By default, Strapi requires authentication to query the API and receive information, but that is outside the scope of this tutorial. Instead, we will make our API publicly accessible. Learn more about Guide on Authenticating Requests with the REST API..

From the left sidebar, click on Settings. Again, on the left panel under USERS & PERMISSIONS PLUGIN, click on Roles, then click on Public from the table on the right. Now scroll down, click on Meeting, and tick Select all. Then click on Transcribed-chunk and do the same. Then save in the top right to allow the user to access information without authentication.

How to connect Strapi with Next.js

Now that the Strapi backend is set up to save and display our transcriptions, let's code the API calls in the frontend and connect between the frontend and backend.

We must create a meeting record every time a user clicks on New meeting on the dashboard. To ensure a record is created in the database, we will first call the API to create a meeting, and once we have confirmation, we will navigate to the transcription page.

Setting Up the API Structure

First, create an api folder in the root directory to manage our API calls. This will help us maintain clean and modular code by keeping all of our API interactions in one place and making them easier to manage.

Inside the api folder, create a file called meetings.js and paste in the following code:

const baseUrl = 'http://localhost:1337';
const url = `${baseUrl}/api/meetings`;

export async function fetchMeetings() {
  try {
    const res = await fetch(url);
    return await res.json();
  } catch (e) {
    console.error('Error fetching meetings:', error);
    throw error;
  }
}

export async function createNewMeeting() {
  // Create new empty meeting
  const payload = {
    data: {
      title: '',
      overview: '',
      ended: false,
      transcribed_chunks: [],
    },
  };

  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(payload),
    });

    return await res.json();
  } catch (error) {
    console.error('Error creating new meeting:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

fetchMeetings will call the Strapi API to get all of the meeting records from the database, and createNewMeeting will create a new empty meeting in the database ready to save information.

Create Custom React Hooks for Meetings

Now that we have a place to keep our API calls, let's create a react hook that will look after some of the logic and state surrounding the meeting collection. This way, if our application grows and we need to do anything with the meeting collection in other components, we can just import and re-use this logic. Conveniently, there will be just one place we need to change the code and one place to test it.

Create and Import the useMeetings Hook

So in the main directory, create a folder called hooks, and inside, create a file called useMeetings.js and paste the following code:

import { useState } from 'react';
import {
  fetchMeetings,
  createNewMeeting,
} from '../api/meetings';

export const useMeetings = () => {
  const [meetings, setMeetings] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const getMeetings = async () => {
    try {
      const response = await fetchMeetings();
      const { data } = await response;

      setMeetings(data);
      setLoading(false);
    } catch (error) {
      setError(error);
      setLoading(false);
    }
  };

  const createMeeting = async () => {
    try {
      const response = await createNewMeeting();
      const { data } = await response;

      return data;
    } catch (error) {
      setError(error);
    }
  };


  return {
    meetings,
    createMeeting,
    getMeetings,
    loading,
    error,
  };
};
Enter fullscreen mode Exit fullscreen mode

Now, let's import this logic in to our main MeetingDashboardContainer:

import React, { useEffect } from 'react';
import styles from '../styles/Meeting.module.css';
import MeetingCard from '../components/meeting/MeetingCard';
import { useRouter } from 'next/router';
import { useMeetings } from '../hooks/useMeetings';

const MeetingDashboardContainer = () => {
  const router = useRouter();
  const {
    meetings,
    loading,
    error,
    getMeetings,
    createMeeting,
  } = useMeetings();

  useEffect(() => {
    fetchMeetings();
  }, []);

  const fetchMeetings = async () => {
    await getMeetings();
  };

  const handleNewMeeting = async () => {
    try {
      // Call the API to create a new meeting
      const newMeeting = await createMeeting();

      // Route to transcription page and pass the newly created meeting id
      router.push(`/transcription?meetingId=${newMeeting.id}`);
    } catch (error) {
      console.error('Error creating new meeting:', error);
      alert('Failed to create a new meeting. Please try again.');
    }
  };

  const openMeeting = (meetingId) => {
    router.push(`/transcription?meetingId=${meetingId}`);
  };


  return (
    <div id={styles['meeting-container']}>
      <div className={styles['cs-container']}>
        <div className={styles['cs-content']}>
          <div className={styles['cs-content-flex']}>
            <span className={styles['cs-topper']}>Meeting dashboard</span>
            <h2 className={styles['cs-title']}>Start a new meeting!</h2>
          </div>
          <button
            onClick={handleNewMeeting}
            className={styles['cs-button-solid']}
          >
            New meeting
          </button>
        </div>
        <ul className={styles['cs-card-group']}>
          {loading ? (
            <p>Loading...</p>
          ) : error ? (
            <p>Error loading previous meetings</p>
          ) : (
            meetings?.map((val, i) => {
              const { title, overview } = val.attributes;
              return (
                <MeetingCard
                  key={val.id}
                  title={title}
                  id={val.id}
                  overview={overview}
                  openMeeting={openMeeting}
                />
              );
            })
          )}
        </ul>
      </div>
    </div>
  );
};

export default MeetingDashboardContainer;
Enter fullscreen mode Exit fullscreen mode

In the useEffect lifecycle hook:

  1. We are fetching any previous meetings to display when the component first mounts.
  2. We have a function called handleNewMeeting used when the user clicks the new meeting button.
  3. This creates a new meeting using the function from our hook.
  4. This then routes to the transcription component and passes through the newly created meetingId

Open Previous Meetings Using the openMeeting Function

We also have a function called openMeeting passed to the MeetingCard component. This allows us to open previous meetings by routing to the transcription component and passing through the meetingId.

Update the MeetingCard component like so:

import styles from '../../styles/Meeting.module.css';

const MeetingCard = ({ title, id, overview, openMeeting }) => {
  return (
    <li className={styles['cs-item']}>
      <div className={styles['cs-flex']}>
        <div style={{ display: 'flex', width: '100%' }}>
          <h3 className={styles['cs-h3']}>{title}</h3>
        </div>
        <p className={styles['cs-item-text']}>{overview}</p>
        <p onClick={() => openMeeting(id)} className={styles['cs-link']}>
          Open meeting
          <img
            className={styles['cs-arrow']}
            loading="lazy"
            decoding="async"
            src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Icons/event-chevron.svg"
            alt="icon"
            width="20"
            height="20"
            aria-hidden="true"
          />
        </p>
      </div>
    </li>
  );
};

export default MeetingCard;
Enter fullscreen mode Exit fullscreen mode

Fetch Meeting Details Using the fetchMeetingDetails Function

Now, because we are passing the meetingId to the transcription container, we will need a way to request the details of a specific meeting using the id; let's add this functionality to the app.

First, let's create another fetch request in our meetings.js API file called fetchMeetingDetails:

export async function fetchMeetingDetails(meetingId) {
  try {
    const response = await fetch(
      `${baseUrl}/api/meetings/${meetingId}?populate=*`
    );

    return await response.json();
  } catch (error) {
    console.error('Error fetching meeting details:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

This function takes the meetingId and interpolates it into the URL. We also add a parameter ?populate=* to populate any relations. In our case, this will be any transcribed_chunks our meeting has saved.

Let's add this API call to our useMeetings.js hooks file so we can import it in and use it easily in the TranscribeContainer.js

We just need to make sure we import it at the top:

import {
  fetchMeetings,
  fetchMeetingDetails,
  createNewMeeting,
} from '../api/meetings';
Enter fullscreen mode Exit fullscreen mode

Add useState so we can save the details:

const [meetingDetails, setMeetingDetails] = useState({});
Enter fullscreen mode Exit fullscreen mode

Paste this new function in, which just uses the API to get the details with the meetingId:

const getMeetingDetails = async (meetingId) => {
    setLoading(true);
    try {
      const response = await fetchMeetingDetails(meetingId);
      const { data } = await response;

      setLoading(false);
      setMeetingDetails(data.attributes);
    } catch (error) {
      setError(error);
      setLoading(false);
    }
  };
Enter fullscreen mode Exit fullscreen mode

And export the function and state in the return statement:

return {
    meetings,
    getMeetingDetails,
    createMeeting,
    getMeetings,
    loading,
    error,
    meetingDetails,
  };
Enter fullscreen mode Exit fullscreen mode

Now, when we route to the transcription container we can use this hook to fetch the individual meeting details with our new function easily, replace the code in TranscribeContainer with the following:

import React, { useState, useEffect } from 'react';
import styles from '../styles/Transcribe.module.css';
import { useAudioRecorder } from '../hooks/useAudioRecorder';
import RecordingControls from '../components/transcription/RecordingControls';
import TranscribedText from '../components/transcription/TranscribedText';
import { useRouter } from 'next/router';
import { useMeetings } from '../hooks/useMeetings';

const mockAnswer =
  'Example answer to transcription here: Lorem ipsum dolor sit amet consectetur adipisicing elit. Velit distinctio quas asperiores reiciendis! Facilis quia recusandae velfacere delect corrupti!';
const mockAnalysis =
  'Example analysis to transcription here: Lorem ipsum dolor sit amet consectetur adipisicing elit. Velit distinctio quas asperiores reiciendis! Facilis quia recusandae velfacere delect corrupti!';

const TranscribeContainer = ({ streaming = true, timeSlice = 1000 }) => {
  const router = useRouter();
  const [analysis, setAnalysis] = useState('');
  const [answer, setAnswer] = useState('');
  const [meetingId, setMeetingId] = useState(null);
  const [meetingTitle, setMeetingTitle] = useState('');
  const {
    getMeetingDetails,
    loading,
    error,
    meetingDetails,
  } = useMeetings();
  const apiKey = process.env.NEXT_PUBLIC_OPENAI_API_KEY;
  const whisperApiEndpoint = 'https://api.openai.com/v1/audio/';
  const { recording, transcribed, handleStartRecording, handleStopRecording } =
    useAudioRecorder(streaming, timeSlice, apiKey, whisperApiEndpoint);

  useEffect(() => {
    const fetchDetails = async () => {
      if (router.isReady) {
        const { meetingId } = router.query;
        if (meetingId) {
          try {
            await getMeetingDetails(meetingId);
            setMeetingId(meetingId);
          } catch (err) {
            console.log('Error getting meeting details - ', err);
          }
        }
      }
    };

    fetchDetails();
  }, [router.isReady, router.query]);

  useEffect(() => {
    setMeetingTitle(meetingDetails.title);
  }, [meetingDetails]);

  const handleGetAnalysis = () => {
    setAnalysis(mockAnalysis);
  };

  const handleGetAnswer = () => {
    setAnswer(mockAnswer);
  };

  const handleStopMeeting = () => {};

  if (loading) return <p>Loading...</p>;

  return (
    <div style={{ margin: '20px' }}>
      <button
        className={styles['end-meeting-button']}
        onClick={handleStopMeeting}
      >
        End Meeting
      </button>

       <input
          onChange={(e) => setMeetingTitle(e.target.value)}
          value={meetingTitle}
          type="text"
          placeholder="Meeting title here..."
          className={styles['custom-input']}
        />
      <div>
        <RecordingControls
          handleStartRecording={handleStartRecording}
          handleStopRecording={handleStopRecording}
        />
        {recording ? (
          <p className={styles['primary-text']}>Recording</p>
        ) : (
          <p>Not recording</p>
        )}
        {transcribed && <h1>Current transcription</h1>}
        <TranscribedText
          transcribed={transcribed}
          answer={answer}
          analysis={analysis}
          handleGetAnalysis={handleGetAnalysis}
          handleGetAnswer={handleGetAnswer}
        />
      </div>
    </div>
  );
};

export default TranscribeContainer;
Enter fullscreen mode Exit fullscreen mode

In the lifecycle hook useEffect above:

  1. We destructure the meetingId parameter passed to the router.
  2. Pass it to the getMeetingDetails function in our hook.
  3. Once that asynchronous request is complete, we set the meeting ID in the state, and the meeting details will now be available to us from the hook.

We will use this to display things like the title and meeting transcriptions.

Create a New Transcription in Strapi

Now, we can start a new meeting, which creates a new record in the database. We have the meetingId in the TranscribeContainer component, which we can use to call the API to save the transcribed_chunks to that particular meeting.

Implement the createNewTranscription Function

First, we need a way to create a new transcribed_chunk. Let's create another file called transcriptions.js in the api directory with the following code:

const baseUrl = 'http://localhost:1337';
const url = `${baseUrl}/api/transcribed-chunks`;

export async function createNewTranscription(transcription) {
  const payload = {
    data: {
      text: transcription,
      analysis: '',
      answer: '',
    },
  };

  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(payload),
    });

    return await res.json();
  } catch (error) {
    console.error('Error saving meeting:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

This POST request will create a new transcribed_chunk record in the database with the text field filled with the transcribed text. We will call the request and then take the ID of the new transcription to link to our meeting.

Link a Transcription to a Meeting

Let's create the API call to link the transcribed text to our meeting. Paste the following code into meetings.js:

export async function connectTranscriptionToMeeting(
  meetingId,
  meetingTitle,
  transcriptionId
) {
  const updateURL = `${baseUrl}/api/meetings/${meetingId}`;
  const payload = {
    data: {
      title: meetingTitle,
      transcribed_chunks: {
        connect: [transcriptionId],
        position: { start: true },
      },
    },
  };

  try {
    const res = await fetch(updateURL, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(payload),
    });

    return await res.json();
  } catch (error) {
    console.error('Error connecting transcription to meeting:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

We are linking the transcribed text to our meeting by:

  1. Using a PUT request to update our meeting,
  2. Using the connect parameter to save our newly created transcribed_chunk to our meeting.
  3. Then, we use the position argument to define the order of our relations. In our case, every new transcription will be saved to the start of the array.

You can read more about how to handle relations in Strapi here.

Open up the useMeetings hook file and paste the following function in to handle this new functionality:

const saveTranscriptionToMeeting = async (meetingId, meetingTitle, transcriptionId) => {
    try {
      await connectTranscriptionToMeeting(meetingId, meetingTitle, transcriptionId);
    } catch (error) {
      setError(error);
    }
  };
Enter fullscreen mode Exit fullscreen mode

Like before, import the method connectTranscriptionToMeeting and return thesaveTranscriptionToMeeting from the hook.

Implement Saving Transcriptions

Now, we're ready to start saving our transcriptions to our meetings. Open TranscribeContainer and import the createNewTranscription method at the top of the file.

import { createNewTranscription } from '../api/transcriptions';
Enter fullscreen mode Exit fullscreen mode

Add saveTranscriptionToMeeting to the list of methods we are destructuring from the useMeetings hook, and add the following async function to the component:

const stopAndSaveTranscription = async () => {
    // save transcription first
    let {
      data: { id: transcriptionId },
    } = await createNewTranscription(transcribed);

    // make a call to save the transcription chunk here
    await saveTranscriptionToMeeting(meetingId, meetingTitle, transcriptionId);
    // re-fetch current meeting which should have updated transcriptions
    await getMeetingDetails(meetingId);
    // Stop and clear the current transcription as it's now saved
    handleStopRecording();
  };
Enter fullscreen mode Exit fullscreen mode
  1. We save the transcription and await the call to destructure the ID.
  2. Then, we save the transcription to our meeting by creating the relation.
  3. We re-fetch the meeting details so we can display a history of the transcriptions
  4. We call handleStopRecording to stop the recording.

Display History of Transcriptions

Now, we will want to display the history of transcription, which we can get from meetingdetails. Add the following variable to the top of TranscribeContainer under the useMeetings hook:

const transcribedHistory = meetingDetails?.transcribed_chunks?.data;
Enter fullscreen mode Exit fullscreen mode

Here, we access the data from the transcribed_chunks array and assign it to a variable with a more readable name.

And then, in the return statement of TranscribeContainer under the TranscribedText add the following:

{/*Transcribed history*/}
<h1>History</h1>
{transcribedHistory?.map((val, i) => {
    const transcribedChunk = val.attributes;
    return (
        <TranscribedText
          key={i}
          transcribed={transcribedChunk.text}
          answer={transcribedChunk.answer}
          analysis={transcribedChunk.analysis}
          handleGetAnalysis={handleGetAnalysis}
          handleGetAnswer={handleGetAnswer}
        />
      );
    })}
Enter fullscreen mode Exit fullscreen mode

You should now be able to view the history of transcriptions as demonstrated below:

Screenshot 2024-06-20 at 11.28.26

Implement Function to End a Meeting

Now, let's add a way for the user to end the meeting, which will lock it to further transcriptions by changing the ended parameter on the meeting to true. We can also request a meeting overview, which will be covered later in this series.

Under the api directory in meetings.js add the following method, which will update our meetings:

export async function updateMeeting(updatedMeeting, meetingId) {
  const updateURL = `${baseUrl}/api/meetings/${meetingId}`;
  const payload = {
    data: updatedMeeting,
  };

  try {
    const res = await fetch(updateURL, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(payload),
    });

    return await res.json();
  } catch (error) {
    console.error('Error updating meeting:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

With this PUT request, we can just pass our meeting object with the fields we want to be updated to this method, and Strapi will update any fields we include and leave the rest as they are. For instance, we could include updated values for ended and overview, but transcribed_chunks will be left as they are.

Add the following function to the useMeetings hook and remember to import the API method and return this function:

const updateMeetingDetails = async (updatedMeeting, meetingId) => {
    try {
      await updateMeeting(updatedMeeting, meetingId);
    } catch (error) {
      setError(error);
    }
  };
Enter fullscreen mode Exit fullscreen mode

Now add the following function to the TranscribeContainer component, remembering to destructure the updateMeetingDetails method from the useMeetings hook:

const handleStopMeeting = async () => {
    await updateMeetingDetails(
      {
        title: meetingTitle,
        ended: true,
      },
      meetingId
    );

    // re-fetch meeting details
    await getMeetingDetails(meetingId);
    setTranscribed("")
  };
Enter fullscreen mode Exit fullscreen mode

Just below the useMeetings hook in this component add the following variable:

const { ended } = meetingDetails;
Enter fullscreen mode Exit fullscreen mode

The above code is just destructuring ended from meetingDetails.

Now replace the code in the return statement for TranscribeContainer as below:

return (
    <div style={{ margin: '20px' }}>
      {ended && (
        <button onClick={handleGoBack} className={styles.goBackButton}>
          Go Back
        </button>
      )}
      {!ended && (
        <button
          className={styles['end-meeting-button']}
          onClick={handleStopMeeting}
        >
          End Meeting
        </button>
      )}
      {ended ? (
        <p className={styles.title}>{meetingTitle}</p>
      ) : (
        <input
          onChange={(e) => setMeetingTitle(e.target.value)}
          value={meetingTitle}
          type="text"
          placeholder="Meeting title here..."
          className={styles['custom-input']}
        />
      )}
      <div>
        {!ended && (
          <div>
            <RecordingControls
              handleStartRecording={handleStartRecording}
              handleStopRecording={stopAndSaveTranscription}
            />
            {recording ? (
              <p className={styles['primary-text']}>Recording</p>
            ) : (
              <p>Not recording</p>
            )}
          </div>
        )}

        {/*Current transcription*/}
        {transcribed && <h1>Current transcription</h1>}
        <TranscribedText
          transcribed={transcribed}
          answer={answer}
          analysis={analysis}
          handleGetAnalysis={handleGetAnalysis}
          handleGetAnswer={handleGetAnswer}
        />

        {/*Transcribed history*/}
        <h1>History</h1>
        {transcribedHistory?.map((val, i) => {
          const transcribedChunk = val.attributes;
          return (
            <TranscribedText
              key={i}
              transcribed={transcribedChunk.text}
              answer={transcribedChunk.answer}
              analysis={transcribedChunk.analysis}
              handleGetAnalysis={handleGetAnalysis}
              handleGetAnswer={handleGetAnswer}
            />
          );
        })}
      </div>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

These are just some minor changes to the UI. If the meeting has ended, we hide the end meeting button and transcription controls, lock the title for further editing, and show the go back button.

Add the following method so the user can navigate back to the dashboard once they have ended the meeting:

 const handleGoBack = () => {
    router.back();
  };
Enter fullscreen mode Exit fullscreen mode

end meeting demo.gif

Delete Recordings of a Meeting

Let's add a way for the user to delete meetings from the dashboard.

Under the api directory in the meetings.js file add the following method:

export async function deleteMeeting(meetingId) {
  try {
    const response = await fetch(`${baseUrl}/api/meetings/${meetingId}`, {
      method: 'DELETE',
    });

    return await response.json();
  } catch (error) {
    console.error('Error deleting meeting:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

We just send a DELETE request with the meetingId through to the Strapi API.

Add the following function to the useMeetings hook, remembering to import the method from the API directory, and return our new function from the hook:

const deleteMeetingDetails = async (meetingId) => {
    try {
      await deleteMeeting(meetingId);
    } catch (error) {
      setError(error);
    }
  };
Enter fullscreen mode Exit fullscreen mode

In the MeetingDashboardContainer file, first destructure deleteMeetingDetails from the useMeeting hook, then add the following function:

const deleteMeeting = async (meetingId) => {
    try {
      await deleteMeetingDetails(meetingId);
      // refetch meeting dashboard data
      await getMeetings();
    } catch (error) {
      console.error('Error deleting meeting:', error);
      alert('Failed to delete meeting. Please try again.');
    }
  };
Enter fullscreen mode Exit fullscreen mode

Now pass deleteMeeting to the MeetingCard component and replace its code with the following:

import styles from '../../styles/Meeting.module.css';

const MeetingCard = ({ title, id, overview, openMeeting, deleteMeeting }) => {
  return (
    <li className={styles['cs-item']}>
      <div className={styles['cs-flex']}>
        <div style={{ display: 'flex', width: '100%' }}>
          <h3 className={styles['cs-h3']}>{title}</h3>
          <div
            onClick={() => deleteMeeting(id)}
            style={{
              marginLeft: 'auto',
              cursor: 'pointer',
              padding: '5px',
            }}
          >
            X
          </div>
        </div>
        <p className={styles['cs-item-text']}>{overview}</p>
        <p onClick={() => openMeeting(id)} className={styles['cs-link']}>
          Open meeting
          <img
            className={styles['cs-arrow']}
            loading="lazy"
            decoding="async"
            src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Icons/event-chevron.svg"
            alt="icon"
            width="20"
            height="20"
            aria-hidden="true"
          />
        </p>
      </div>
    </li>
  );
};

export default MeetingCard;
Enter fullscreen mode Exit fullscreen mode

Now, the user can create new meetings and save information such as transcriptions, titles, etc. They can also end meetings, locking out any further editing. They also now have the ability to delete meetings when they are no longer relevant.

delete meeting demo.gif

Stay Tuned for Part 3

In part three of this series, we will create custom endpoints to link our transcription app to chatGPT. We will also run through some testing, error handling, and how to deploy the app to Strapi cloud.

Additional Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .