Lecture 55 - Clean YouTube Project | Display a List of Playlists
গত লেকচারে আমরা আমাদের এপিআই লজিক এবং হুক বানিয়েছিলাম। আমরা আজ চেষ্টা করবো প্লেলিস্টের একটা লিস্ট যেন ডিসপ্লে করতে পারি। চলুন শুরু করে দিই।
Error Handling
ধরুন কোনো কারণে এরর আসলো। হতে পারে সেটা এপিআই কী প্রোভাইড না করার কারণে বা অন্য কোনো কারণে। এখন এরর হ্যান্ডলিং না করাটা একটা ব্যাড প্র্যাকটিস। আমরা এখন শুধু লজিক লিখেছি প্লেলিস্ট রিটার্ন করার। কিন্তু কোনো কারণে রিটার্ন না করলে সেটা আমাদের ইউজারকে অবশ্যই জানাতে হবে কেন প্লেলিস্ট রিটার্ন হচ্ছে না। আমরা যে usePlaylists
হুক বানিয়েছিলাম সেখানে এখন এরর হ্যান্ডলিং করবো।
import { useState } from 'react';
import getPlaylist from '../api';
const usePlaylists = () => {
const [state, setState] = useState({
playlists: {},
recentPlaylists: [],
favorites: [],
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const getPlaylistById = async (playlistId, force = false) => {
if (state.playlists[playlistId] && !force) {
return;
}
// Following lines had removed
/*
let result = await getPlaylist(playlistId);
let cid, ct;
result = result.map((item) => {
const {
channelId,
title,
description,
thumbnails: { medium },
channelTitle,
} = item.snippet;
if (!cid) {
cid = channelId;
}
if (!ct) {
ct = channelTitle;
}
return {
title,
description,
thumbnail: medium,
contentDetails: item.contentDetails,
};
});
setState((prev) => ({
...prev,
playlists: {
...prev.playlists,
[playlistId]: {
items: result,
playlistId: playlistId,
channelId: cid,
channelTitle: ct,
},
},
}));
*/
// Following lines had added
setLoading(true);
try {
const playlist = await getPlaylist(playlistId);
setError('');
setState((prev) => ({
...prev,
playlists: {
...prev.playlists,
[playlistId]: playlist,
},
}));
} catch (e) {
setError(
e.response?.data?.error?.message || 'Something went wrong'
);
} finally {
setLoading(false);
}
};
const addToFavorites = (playlistId) => {
setState((prev) => ({
...prev,
favorites: [...prev, playlistId],
}));
};
const addToRecent = (playlistId) => {
setState((prev) => ({
...prev,
recentPlaylists: [...prev, playlistId],
}));
};
const getPlaylistsByIds = (ids = []) => {
return ids.map((id) => state.playlists[id]);
};
return {
playlists: state.playlists,
favorites: getPlaylistsByIds(state.favorites),
recentPlaylists: getPlaylistsByIds(state.recentPlaylists),
error,
loading,
getPlaylistById,
addToRecent,
addToFavorites,
আমরা প্রথমে error
এবং loading
নামে দুইটা স্টেট নিলাম। এরপর উপরের দেখানো কোড রিমুভ করে দিলাম। এরপর setLoading(true)
করে দিলাম। try
ব্লকে প্রথমে আমরা প্লেলিস্ট ফেচ করে নিলাম। এরপর স্টেটের মধ্যে আমরা সেই প্লেলিস্টকে অ্যাড করে দিলাম। এবার catch
ব্লকে আমরা যদি প্লেলিস্ট পাওয়া না যায় সেক্ষেত্রে কি হবে তার জন্য এরর হ্যান্ডলিং করে নিলাম। আমরা এখানে যেহেতু axios
ব্যবহার করছি, সেজন্য এরর ম্যাসেজ e.response.data.error.message
এর মধ্যে পাওয়া যাবে। অন্য প্যাকেজ ব্যবহার করলে সেই অনুসারে আমাদের এটা পরিবর্তন হবে। এখন আমরা লিখেছি e.response?.data?.error?.message
এর মানে হলো যেখানে আমরা ?
ব্যবহার করেছি তার বামপাশের অবজেক্ট যদি undefined
থাকে তাহলে সে আমাদেরকে কোনো রানটাইম এরর থ্রো করবে না। সে আমাদের ম্যাসেজ দিবে something went wrong
। এই ?
সাইন ব্যবহার করা হয় শর্ট সার্কিটের ক্ষেত্রে। একে বলা হয় optional chaining
। সবশেষে finally
ব্লকের মধ্যে আমরা setLoading(false)
করে দিবো। আর একদম শেষে আমরা error
এবং loading
স্টেটকে রিটার্ন করে দিলাম।
API Logic Modification
আমরা গতদিন প্লেলিস্ট আইডি পেয়েছি। কিন্তু প্লেলিস্টের নাম, থাম্বনেইল কিছুই পাইনি। সেটার জন্য আমাদের একটু এপিআই লজিক পরিবর্তন করতে হবে। আমরা যে getPlaylist
নামে ফাংশন লিখেছিলাম সেটাকে পরিবর্তন করে getPlaylistItem
করে দিলাম। এবং getPlaylist
নামে আরেকটি ফাংশন লিখলাম যেখান থেকে আমরা playlistId
, playlistTitle
, playlistDescription
, playlistThumbnail
, channelId
, channelTitle
, playlistItems
রিটার্ন করে দিবো।
import axios from 'axios';
const key = import.meta.env.VITE_YOUTUBE_API_KEY;
const getPlaylistItem = async (playlistId, pageToken = '', result = []) => {
const URL = `https://www.googleapis.com/youtube/v3/playlistItems?key=${key}&part=id,contentDetails,snippet&maxResults=50&playlistId=${playlistId}&pageToken=${pageToken}`;
const { data } = await axios.get(URL);
result = [...result, ...data.items];
if (data.nextPageToken) {
result = getPlaylistItem(playlistId, data.nextPageToken, result);
}
return result;
};
const getPlaylist = async (playlistId) => {
const URL = `https://youtube.googleapis.com/youtube/v3/playlists?part=snippet&id=${playlistId}&key=${key}`;
const { data } = await axios.get(URL);
let playlistItems = await getPlaylistItem(playlistId);
const {
title: playlistTitle,
description: playlistDescription,
thumbnails,
channelId,
channelTitle,
} = data?.items[0]?.snippet;
playlistItems = playlistItems.map((item) => {
const {
title,
description,
thumbnails: { medium },
} = item.snippet;
return {
title,
description,
thumbnail: medium,
contentDetails: item.contentDetails,
};
});
return {
playlistId,
playlistTitle,
playlistDescription,
playlistThumbnail: thumbnails.default,
channelId,
channelTitle,
playlistItems,
};
};
export default getPlaylist;
result = getPlaylistItem(playlistId, data.nextPageToken, result);
এই লাইনে সোর্স কোডে ভুলক্রমে getPlaylistItem
এর জায়গায় getPlaylist
দেয়া আছে। আপনারা সেটা শুদ্ধ করে নিবেন।
MUI Installation
এবার আমরা MUI ইনস্টল করবো।
yarn add @mui/material @emotion/react @emotion/styled @mui/icons-material
Navbar Component
এবার আমরা ন্যাভবার কম্পোনেন্ট তৈরি করবো। আমরা /src/components/navbar/index.jsx
এর মধ্যে গিয়ে নিচের কোড লিখবো।
import { useState } from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import AccountCircle from '@mui/icons-material/AccountCircle';
import Switch from '@mui/material/Switch';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormGroup from '@mui/material/FormGroup';
import MenuItem from '@mui/material/MenuItem';
import Menu from '@mui/material/Menu';
import { Button, Stack } from '@mui/material';
import { Container } from '@mui/system';
import PlaylistForm from '../playlist-form';
const Navbar = ({ getPlaylistById }) => {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const getPlaylistId = (playlistId) => {
getPlaylistById(playlistId);
};
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position='fixed' color='default' sx={{ py: 2 }}>
<Container maxWidth={'lg'}>
<Toolbar>
<Stack sx={{ flexGrow: 1 }}>
<Typography variant='h4'>Clean Youtube</Typography>
<Typography variant='body1'>
By Stack Learner
</Typography>
</Stack>
<Button variant='contained' onClick={handleClickOpen}>
Add Playlist
</Button>
<PlaylistForm
open={open}
handleClose={handleClose}
getPlaylistId={getPlaylistId}
/>
</Toolbar>
</Container>
</AppBar>
</Box>
);
};
export default Navbar;
ব্যাখ্যা করলাম না, কারণ এখানে আশা করি সবাই বুঝতে পারবেন। PlaylistForm
আমরা এখনই তৈরি করছি।
PlaylistForm Component
এবার আমরা /src/components/playlist-form/index.jsx
এর মধ্যে গিয়ে নিচের কম্পোনেন্ট বানাবো।
import { useState } from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
const PlaylistForm = ({ open, handleClose, getPlaylistId }) => {
const [state, setState] = useState('');
const handleSubmit = () => {
// TODO: handle url later
if (!state) {
alert('Invalid State');
} else {
getPlaylistId(state);
setState('');
handleClose();
}
};
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Add Playlist</DialogTitle>
<DialogContent>
<DialogContentText>
To add a new playlist please insert the playlist id or
playlist link. Please make sure the link is correct.
Otherwise we won't able to fetch the playlist information.
</DialogContentText>
<TextField
autoFocus
margin='dense'
label='Playlist ID or Link'
fullWidth
variant='standard'
onChange={(e) => setState(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleSubmit}>Add Playlist</Button>
</DialogActions>
</Dialog>
);
};
export default PlaylistForm;
PlaylistCardItem Component
আমরা src/components/playlist-card-item/index.jsx
নামে একটা ফাইল নিয়ে নিলাম।
import * as React from 'react';
import Card from '@mui/material/Card';
import CardMedia from '@mui/material/CardMedia';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Typography from '@mui/material/Typography';
import { Box, Button, Stack } from '@mui/material';
import { PlayCircleOutline } from '@mui/icons-material';
const PlaylistCardItem = ({
playlistThumbnail,
playlistTitle,
channelTitle,
}) => {
return (
<Card
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
margin: 1,
}}
>
<CardMedia
component='img'
image={playlistThumbnail.url}
alt={playlistTitle}
/>
<CardContent>
<Typography variant='h6' color='text.primary'>
{`${
playlistTitle.length > 50
? playlistTitle.substr(0, 50) + '...'
: playlistTitle
}`}
</Typography>
<Typography variant='body2' color='text.secondary'>
{channelTitle}
</Typography>
</CardContent>
<Box sx={{ flexGrow: 1 }}></Box>
<CardActions disableSpacing>
<Button>
<Stack direction={'row'} spacing={1} alignItems={'center'}>
<PlayCircleOutline />
<Typography variant='body2' fontWeight={600}>
Start Tutorial
</Typography>
</Stack>
</Button>
</CardActions>
</Card>
);
};
export default PlaylistCardItem;
Importing all components to App.jsx
এবার আমরা সমস্ত কম্পোনেন্টকে আমাদের App.jsx
এর মধ্যে ইমপোর্ট করে নিবো।
mport { Grid, Stack } from '@mui/material';
import CssBaseline from '@mui/material/CssBaseline';
import { Container } from '@mui/system';
import Navbar from './components/navbar';
import PlaylistCardItem from './components/playlist-card-item';
import usePlaylists from './hooks/usePlaylists';
const App = () => {
const { playlists, error, getPlaylistById } = usePlaylists();
const playlistArray = Object.values(playlists);
return (
<>
<CssBaseline />
<Container maxWidth={'lg'} sx={{ my: 16 }}>
<Navbar getPlaylistById={getPlaylistById} />
{playlistArray.length > 0 && (
<Grid container alignItems='stretch'>
{playlistArray.map((item) => (
<Grid item xs={12} md={6} lg={4} mb={2}>
<PlaylistCardItem
key={item.id}
playlistThumbnail={item.playlistThumbnail}
playlistTitle={item.playlistTitle}
channelTitle={item.channelTitle}
/>
</Grid>
))}
</Grid>
)}
</Container>
</>
);
};
Final View
Source Code
এই লেকচারের সকল সোর্স কোড এই লিংক এ পাবেন।