Lecture 55 - Clean YouTube Project | Display a List of Playlists

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

এই লেকচারের সকল সোর্স কোড এই লিংক এ পাবেন।