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.
You can find the outline for this series below:
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
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:
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:
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:
1const meeting = {
2 title: "",
3 overview: "",
4 ended: false,
5 transcribedChunks: [
6 {
7 text: "",
8 analysis: "",
9 answer: ""
10 },
11 {
12 text: "",
13 analysis: "",
14 answer: ""
15 }
16 ]
17};
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 all three of the fields mentioned above:
text
analysis
answer
with a type of Long text: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 the following fields: a text field for the
title
(Short Text)overview
(Long Text)ended
(relation:meeting has many transcribed-chunks
):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.
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.
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.
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:
1const baseUrl = 'http://localhost:1337';
2const url = `${baseUrl}/api/meetings`;
3
4export async function fetchMeetings() {
5 try {
6 const res = await fetch(url);
7 return await res.json();
8 } catch (e) {
9 console.error('Error fetching meetings:', error);
10 throw error;
11 }
12}
13
14export async function createNewMeeting() {
15 // Create new empty meeting
16 const payload = {
17 data: {
18 title: '',
19 overview: '',
20 ended: false,
21 transcribed_chunks: [],
22 },
23 };
24
25 try {
26 const res = await fetch(url, {
27 method: 'POST',
28 headers: {
29 'Content-Type': 'application/json',
30 },
31 body: JSON.stringify(payload),
32 });
33
34 return await res.json();
35 } catch (error) {
36 console.error('Error creating new meeting:', error);
37 throw error;
38 }
39}
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.
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.
useMeetings
HookSo in the main directory, create a folder called hooks
, and inside, create a file called useMeetings.js
and paste the following code:
1import { useState } from 'react';
2import {
3 fetchMeetings,
4 createNewMeeting,
5} from '../api/meetings';
6
7export const useMeetings = () => {
8 const [meetings, setMeetings] = useState([]);
9 const [loading, setLoading] = useState(true);
10 const [error, setError] = useState(null);
11
12 const getMeetings = async () => {
13 try {
14 const response = await fetchMeetings();
15 const { data } = await response;
16
17 setMeetings(data);
18 setLoading(false);
19 } catch (error) {
20 setError(error);
21 setLoading(false);
22 }
23 };
24
25 const createMeeting = async () => {
26 try {
27 const response = await createNewMeeting();
28 const { data } = await response;
29
30 return data;
31 } catch (error) {
32 setError(error);
33 }
34 };
35
36
37 return {
38 meetings,
39 createMeeting,
40 getMeetings,
41 loading,
42 error,
43 };
44};
Now, let's import this logic in to our main MeetingDashboardContainer
:
1import React, { useEffect } from 'react';
2import styles from '../styles/Meeting.module.css';
3import MeetingCard from '../components/meeting/MeetingCard';
4import { useRouter } from 'next/router';
5import { useMeetings } from '../hooks/useMeetings';
6
7const MeetingDashboardContainer = () => {
8 const router = useRouter();
9 const {
10 meetings,
11 loading,
12 error,
13 getMeetings,
14 createMeeting,
15 } = useMeetings();
16
17 useEffect(() => {
18 fetchMeetings();
19 }, []);
20
21 const fetchMeetings = async () => {
22 await getMeetings();
23 };
24
25 const handleNewMeeting = async () => {
26 try {
27 // Call the API to create a new meeting
28 const newMeeting = await createMeeting();
29
30 // Route to transcription page and pass the newly created meeting id
31 router.push(`/transcription?meetingId=${newMeeting.id}`);
32 } catch (error) {
33 console.error('Error creating new meeting:', error);
34 alert('Failed to create a new meeting. Please try again.');
35 }
36 };
37
38 const openMeeting = (meetingId) => {
39 router.push(`/transcription?meetingId=${meetingId}`);
40 };
41
42
43 return (
44 <div id={styles['meeting-container']}>
45 <div className={styles['cs-container']}>
46 <div className={styles['cs-content']}>
47 <div className={styles['cs-content-flex']}>
48 <span className={styles['cs-topper']}>Meeting dashboard</span>
49 <h2 className={styles['cs-title']}>Start a new meeting!</h2>
50 </div>
51 <button
52 onClick={handleNewMeeting}
53 className={styles['cs-button-solid']}
54 >
55 New meeting
56 </button>
57 </div>
58 <ul className={styles['cs-card-group']}>
59 {loading ? (
60 <p>Loading...</p>
61 ) : error ? (
62 <p>Error loading previous meetings</p>
63 ) : (
64 meetings?.map((val, i) => {
65 const { title, overview } = val.attributes;
66 return (
67 <MeetingCard
68 key={val.id}
69 title={title}
70 id={val.id}
71 overview={overview}
72 openMeeting={openMeeting}
73 />
74 );
75 })
76 )}
77 </ul>
78 </div>
79 </div>
80 );
81};
82
83export default MeetingDashboardContainer;
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
.
openMeeting
FunctionWe 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:
1import styles from '../../styles/Meeting.module.css';
2
3const MeetingCard = ({ title, id, overview, openMeeting }) => {
4 return (
5 <li className={styles['cs-item']}>
6 <div className={styles['cs-flex']}>
7 <div style={{ display: 'flex', width: '100%' }}>
8 <h3 className={styles['cs-h3']}>{title}</h3>
9 </div>
10 <p className={styles['cs-item-text']}>{overview}</p>
11 <p onClick={() => openMeeting(id)} className={styles['cs-link']}>
12 Open meeting
13 <img
14 className={styles['cs-arrow']}
15 loading="lazy"
16 decoding="async"
17 src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Icons/event-chevron.svg"
18 alt="icon"
19 width="20"
20 height="20"
21 aria-hidden="true"
22 />
23 </p>
24 </div>
25 </li>
26 );
27};
28
29export default MeetingCard;
fetchMeetingDetails
FunctionNow, 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
:
1export async function fetchMeetingDetails(meetingId) {
2 try {
3 const response = await fetch(
4 `${baseUrl}/api/meetings/${meetingId}?populate=*`
5 );
6
7 return await response.json();
8 } catch (error) {
9 console.error('Error fetching meeting details:', error);
10 throw error;
11 }
12}
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:
1import {
2 fetchMeetings,
3 fetchMeetingDetails,
4 createNewMeeting,
5} from '../api/meetings';
Add useState
so we can save the details:
1const [meetingDetails, setMeetingDetails] = useState({});
Paste this new function in, which just uses the API to get the details with the meetingId
:
1const getMeetingDetails = async (meetingId) => {
2 setLoading(true);
3 try {
4 const response = await fetchMeetingDetails(meetingId);
5 const { data } = await response;
6
7 setLoading(false);
8 setMeetingDetails(data.attributes);
9 } catch (error) {
10 setError(error);
11 setLoading(false);
12 }
13 };
And export the function and state in the return statement:
1return {
2 meetings,
3 getMeetingDetails,
4 createMeeting,
5 getMeetings,
6 loading,
7 error,
8 meetingDetails,
9 };
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:
1import React, { useState, useEffect } from 'react';
2import styles from '../styles/Transcribe.module.css';
3import { useAudioRecorder } from '../hooks/useAudioRecorder';
4import RecordingControls from '../components/transcription/RecordingControls';
5import TranscribedText from '../components/transcription/TranscribedText';
6import { useRouter } from 'next/router';
7import { useMeetings } from '../hooks/useMeetings';
8
9const mockAnswer =
10 'Example answer to transcription here: Lorem ipsum dolor sit amet consectetur adipisicing elit. Velit distinctio quas asperiores reiciendis! Facilis quia recusandae velfacere delect corrupti!';
11const mockAnalysis =
12 'Example analysis to transcription here: Lorem ipsum dolor sit amet consectetur adipisicing elit. Velit distinctio quas asperiores reiciendis! Facilis quia recusandae velfacere delect corrupti!';
13
14const TranscribeContainer = ({ streaming = true, timeSlice = 1000 }) => {
15 const router = useRouter();
16 const [analysis, setAnalysis] = useState('');
17 const [answer, setAnswer] = useState('');
18 const [meetingId, setMeetingId] = useState(null);
19 const [meetingTitle, setMeetingTitle] = useState('');
20 const {
21 getMeetingDetails,
22 loading,
23 error,
24 meetingDetails,
25 } = useMeetings();
26 const apiKey = process.env.NEXT_PUBLIC_OPENAI_API_KEY;
27 const whisperApiEndpoint = 'https://api.openai.com/v1/audio/';
28 const { recording, transcribed, handleStartRecording, handleStopRecording } =
29 useAudioRecorder(streaming, timeSlice, apiKey, whisperApiEndpoint);
30
31 useEffect(() => {
32 const fetchDetails = async () => {
33 if (router.isReady) {
34 const { meetingId } = router.query;
35 if (meetingId) {
36 try {
37 await getMeetingDetails(meetingId);
38 setMeetingId(meetingId);
39 } catch (err) {
40 console.log('Error getting meeting details - ', err);
41 }
42 }
43 }
44 };
45
46 fetchDetails();
47 }, [router.isReady, router.query]);
48
49 useEffect(() => {
50 setMeetingTitle(meetingDetails.title);
51 }, [meetingDetails]);
52
53 const handleGetAnalysis = () => {
54 setAnalysis(mockAnalysis);
55 };
56
57 const handleGetAnswer = () => {
58 setAnswer(mockAnswer);
59 };
60
61 const handleStopMeeting = () => {};
62
63 if (loading) return <p>Loading...</p>;
64
65 return (
66 <div style={{ margin: '20px' }}>
67 <button
68 className={styles['end-meeting-button']}
69 onClick={handleStopMeeting}
70 >
71 End Meeting
72 </button>
73
74 <input
75 onChange={(e) => setMeetingTitle(e.target.value)}
76 value={meetingTitle}
77 type="text"
78 placeholder="Meeting title here..."
79 className={styles['custom-input']}
80 />
81 <div>
82 <RecordingControls
83 handleStartRecording={handleStartRecording}
84 handleStopRecording={handleStopRecording}
85 />
86 {recording ? (
87 <p className={styles['primary-text']}>Recording</p>
88 ) : (
89 <p>Not recording</p>
90 )}
91 {transcribed && <h1>Current transcription</h1>}
92 <TranscribedText
93 transcribed={transcribed}
94 answer={answer}
95 analysis={analysis}
96 handleGetAnalysis={handleGetAnalysis}
97 handleGetAnswer={handleGetAnswer}
98 />
99 </div>
100 </div>
101 );
102};
103
104export default TranscribeContainer;
In the lifecycle hook useEffect
above:
meetingId
parameter passed to the router. getMeetingDetails
function in our hook. meeting details
will now be available to us from the hook. We will use this to display things like the title and meeting transcriptions.
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.
createNewTranscription
FunctionFirst, 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:
1const baseUrl = 'http://localhost:1337';
2const url = `${baseUrl}/api/transcribed-chunks`;
3
4export async function createNewTranscription(transcription) {
5 const payload = {
6 data: {
7 text: transcription,
8 analysis: '',
9 answer: '',
10 },
11 };
12
13 try {
14 const res = await fetch(url, {
15 method: 'POST',
16 headers: {
17 'Content-Type': 'application/json',
18 },
19 body: JSON.stringify(payload),
20 });
21
22 return await res.json();
23 } catch (error) {
24 console.error('Error saving meeting:', error);
25 throw error;
26 }
27}
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.
Let's create the API call to link the transcribed text to our meeting. Paste the following code into meetings.js
:
1export async function connectTranscriptionToMeeting(
2 meetingId,
3 meetingTitle,
4 transcriptionId
5) {
6 const updateURL = `${baseUrl}/api/meetings/${meetingId}`;
7 const payload = {
8 data: {
9 title: meetingTitle,
10 transcribed_chunks: {
11 connect: [transcriptionId],
12 position: { start: true },
13 },
14 },
15 };
16
17 try {
18 const res = await fetch(updateURL, {
19 method: 'PUT',
20 headers: {
21 'Content-Type': 'application/json',
22 },
23 body: JSON.stringify(payload),
24 });
25
26 return await res.json();
27 } catch (error) {
28 console.error('Error connecting transcription to meeting:', error);
29 throw error;
30 }
31}
We are linking the transcribed text to our meeting by:
PUT
request to update our meeting,connect
parameter to save our newly created transcribed_chunk
to our meeting.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:
1const saveTranscriptionToMeeting = async (meetingId, meetingTitle, transcriptionId) => {
2 try {
3 await connectTranscriptionToMeeting(meetingId, meetingTitle, transcriptionId);
4 } catch (error) {
5 setError(error);
6 }
7 };
Like before, import the method connectTranscriptionToMeeting
and return thesaveTranscriptionToMeeting
from the hook.
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.
1import { createNewTranscription } from '../api/transcriptions';
Add saveTranscriptionToMeeting
to the list of methods we are destructuring from the useMeetings
hook, and add the following async function to the component:
1const stopAndSaveTranscription = async () => {
2 // save transcription first
3 let {
4 data: { id: transcriptionId },
5 } = await createNewTranscription(transcribed);
6
7 // make a call to save the transcription chunk here
8 await saveTranscriptionToMeeting(meetingId, meetingTitle, transcriptionId);
9 // re-fetch current meeting which should have updated transcriptions
10 await getMeetingDetails(meetingId);
11 // Stop and clear the current transcription as it's now saved
12 handleStopRecording();
13 };
handleStopRecording
to stop the recording.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:
1const transcribedHistory = meetingDetails?.transcribed_chunks?.data;
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:
1{/*Transcribed history*/}
2<h1>History</h1>
3{transcribedHistory?.map((val, i) => {
4 const transcribedChunk = val.attributes;
5 return (
6 <TranscribedText
7 key={i}
8 transcribed={transcribedChunk.text}
9 answer={transcribedChunk.answer}
10 analysis={transcribedChunk.analysis}
11 handleGetAnalysis={handleGetAnalysis}
12 handleGetAnswer={handleGetAnswer}
13 />
14 );
15 })}
You should now be able to view the history of transcriptions as demonstrated below:
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:
1export async function updateMeeting(updatedMeeting, meetingId) {
2 const updateURL = `${baseUrl}/api/meetings/${meetingId}`;
3 const payload = {
4 data: updatedMeeting,
5 };
6
7 try {
8 const res = await fetch(updateURL, {
9 method: 'PUT',
10 headers: {
11 'Content-Type': 'application/json',
12 },
13 body: JSON.stringify(payload),
14 });
15
16 return await res.json();
17 } catch (error) {
18 console.error('Error updating meeting:', error);
19 throw error;
20 }
21}
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:
1const updateMeetingDetails = async (updatedMeeting, meetingId) => {
2 try {
3 await updateMeeting(updatedMeeting, meetingId);
4 } catch (error) {
5 setError(error);
6 }
7 };
Now add the following function to the TranscribeContainer
component, remembering to destructure the updateMeetingDetails
method from the useMeetings
hook:
1const handleStopMeeting = async () => {
2 await updateMeetingDetails(
3 {
4 title: meetingTitle,
5 ended: true,
6 },
7 meetingId
8 );
9
10 // re-fetch meeting details
11 await getMeetingDetails(meetingId);
12 setTranscribed("")
13 };
Just below the useMeetings
hook in this component add the following variable:
1const { ended } = meetingDetails;
The above code is just destructuring ended
from meetingDetails
.
Now replace the code in the return statement for TranscribeContainer
as below:
1return (
2 <div style={{ margin: '20px' }}>
3 {ended && (
4 <button onClick={handleGoBack} className={styles.goBackButton}>
5 Go Back
6 </button>
7 )}
8 {!ended && (
9 <button
10 className={styles['end-meeting-button']}
11 onClick={handleStopMeeting}
12 >
13 End Meeting
14 </button>
15 )}
16 {ended ? (
17 <p className={styles.title}>{meetingTitle}</p>
18 ) : (
19 <input
20 onChange={(e) => setMeetingTitle(e.target.value)}
21 value={meetingTitle}
22 type="text"
23 placeholder="Meeting title here..."
24 className={styles['custom-input']}
25 />
26 )}
27 <div>
28 {!ended && (
29 <div>
30 <RecordingControls
31 handleStartRecording={handleStartRecording}
32 handleStopRecording={stopAndSaveTranscription}
33 />
34 {recording ? (
35 <p className={styles['primary-text']}>Recording</p>
36 ) : (
37 <p>Not recording</p>
38 )}
39 </div>
40 )}
41
42 {/*Current transcription*/}
43 {transcribed && <h1>Current transcription</h1>}
44 <TranscribedText
45 transcribed={transcribed}
46 answer={answer}
47 analysis={analysis}
48 handleGetAnalysis={handleGetAnalysis}
49 handleGetAnswer={handleGetAnswer}
50 />
51
52 {/*Transcribed history*/}
53 <h1>History</h1>
54 {transcribedHistory?.map((val, i) => {
55 const transcribedChunk = val.attributes;
56 return (
57 <TranscribedText
58 key={i}
59 transcribed={transcribedChunk.text}
60 answer={transcribedChunk.answer}
61 analysis={transcribedChunk.analysis}
62 handleGetAnalysis={handleGetAnalysis}
63 handleGetAnswer={handleGetAnswer}
64 />
65 );
66 })}
67 </div>
68 </div>
69 );
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:
1 const handleGoBack = () => {
2 router.back();
3 };
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:
1export async function deleteMeeting(meetingId) {
2 try {
3 const response = await fetch(`${baseUrl}/api/meetings/${meetingId}`, {
4 method: 'DELETE',
5 });
6
7 return await response.json();
8 } catch (error) {
9 console.error('Error deleting meeting:', error);
10 throw error;
11 }
12}
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:
1const deleteMeetingDetails = async (meetingId) => {
2 try {
3 await deleteMeeting(meetingId);
4 } catch (error) {
5 setError(error);
6 }
7 };
In the MeetingDashboardContainer
file, first destructure deleteMeetingDetails
from the useMeeting
hook, then add the following function:
1const deleteMeeting = async (meetingId) => {
2 try {
3 await deleteMeetingDetails(meetingId);
4 // refetch meeting dashboard data
5 await getMeetings();
6 } catch (error) {
7 console.error('Error deleting meeting:', error);
8 alert('Failed to delete meeting. Please try again.');
9 }
10 };
Now pass deleteMeeting
to the MeetingCard
component and replace its code with the following:
1import styles from '../../styles/Meeting.module.css';
2
3const MeetingCard = ({ title, id, overview, openMeeting, deleteMeeting }) => {
4 return (
5 <li className={styles['cs-item']}>
6 <div className={styles['cs-flex']}>
7 <div style={{ display: 'flex', width: '100%' }}>
8 <h3 className={styles['cs-h3']}>{title}</h3>
9 <div
10 onClick={() => deleteMeeting(id)}
11 style={{
12 marginLeft: 'auto',
13 cursor: 'pointer',
14 padding: '5px',
15 }}
16 >
17 X
18 </div>
19 </div>
20 <p className={styles['cs-item-text']}>{overview}</p>
21 <p onClick={() => openMeeting(id)} className={styles['cs-link']}>
22 Open meeting
23 <img
24 className={styles['cs-arrow']}
25 loading="lazy"
26 decoding="async"
27 src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Icons/event-chevron.svg"
28 alt="icon"
29 width="20"
30 height="20"
31 aria-hidden="true"
32 />
33 </p>
34 </div>
35 </li>
36 );
37};
38
39export default MeetingCard;
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.
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.
Hey! 👋 I'm Mike, a seasoned web developer with 5 years of full-stack expertise. Passionate about tech's impact on the world, I'm on a journey to blend code with compelling stories. Let's explore the tech landscape together! 🚀✍️