ホーム » Reactを利用したLaravelのyoutube管理アプリを作ってみよう!

Reactを利用したLaravelのyoutube管理アプリを作ってみよう!

				
					
Route::middleware('auth')->prefix('videos')->group(function () {
  Route::get('/', [VideoController::class, 'index'])->name('video.index');
});
				
			
2.コントローラーを編集します。
VideoController.phpを編集します。
				
					
public function index()
{
  $videos = Video::all();
  return Inertia::render('Videos/Index', [
    'videos' => $videos
  ]);
}
				
			

3.ビューを作成します。
Index.jsx

				
					
import React from 'react';
import {Link} from "@inertiajs/react";

const Videos = ({ videos }) => {
    return (
        <div>
            {videos.map((video, index) => (
                <div key={index}>
                    {video.title} - <Link href={`/videos/detail/${video.id}`}>詳細</Link>
                </div>
            ))}

            <Link href={`/videos/create`}>新規作成</Link>
        </div>
    );
};

export default Videos
				
			

詳細画面を作成する

1.ルーティングを作成します。
routes/web.phpに下記を記載してください。

				
					
Route::middleware('auth')->prefix('videos')->group(function () {
  Route::get('/detail/{id}', [VideoController::class, 'show'])->name('video.show');
});
				
			
2.コントローラーを編集します。
VideoController.phpを編集します。
				
					
public function show(string $id)
{
    $video = Video::find($id);
    return Inertia::rende('Videos/Show', [
        'video' => $video
    ]);
}
				
			

3.ビューを作成します。
Show.jsx

				
					
import React from 'react';
import {Link} from "@inertiajs/react";
import {Inertia} from "@inertiajs/inertia";

const Video = ({ video }) => {
    const handleBack = () => {
        window.history.back();
    };

    return (
        <div>
            <p>{video.title}</p>
            <p>{video.text}</p>
            <p>{video.status}</p>
            <p>{video.youtube_url}</p>
            <p><Link href={`/videos/update/${video.id}`}>更新</Link></p>
            <button onClick={handleBack}>戻る</button>
        </div>
    );
};

export default Video

				
			

新規作成画面を作成する

1.ルーティングを作成します。
routes/web.phpに下記を記載してください。

				
					
Route::middleware('auth')->prefix('videos')->group(function () {
  Route::get('/create', [VideoController::class, 'create'])->name('video.create');
  Route::post('/store', [VideoController::class, 'store'])->name('video.store');
});
				
			
2.コントローラーを編集します。
VideoController.phpを編集します。
				
					    
public function create()
{
    return Inertia::rende('Videos/Create', []);
}

public function store(Request $request)
{
    $data = $request->all();
    $data['user_id'] = auth()->id();
    Video::create($data);

    return redirect()->route('video.index');
}
				
			

3.ビューを作成します。
Create.jsx

				
					
import React, {useState} from 'react';
import { Inertia } from '@inertiajs/inertia';

const Create = () => {
    const handleBack = () => {
        window.history.back();
    };

    // useStateはReactのフック(Hook)の一つで、関数コンポーネント内で状態(state)を持たせるために使用されます。
    // 一番右がデフォルト値
    // 左側が値、右型が値を設定するためのもの
    // なので、setFormDataに値を詰めると、formDataとしてformの値が扱える。
    const [formData, setFormData] = useState({
        title: '',
        text: '',
        status: '',
        youtube_url: ''
    });

    // name属性とvalue属性をformDataとして設定する。
    // ここのsetFormDataで、useStateのtitleとかを変更している。
    const handleChange = (e) => {
        setFormData({ ...formData, [e.target.name]: e.target.value });
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log(111)
        Inertia.post('/videos/store', formData)
    }


    return (
        <div>
            <p>Create</p>
            <form onSubmit={handleSubmit}>
                <p>title:</p>
                <input
                    type="text"
                    id="title"
                    name="title"
                    value={formData.title}
                    onChange={handleChange}
                />
                <p>text:</p>
                <input
                    type="text"
                    id="text"
                    name="text"
                    value={formData.text}
                    onChange={handleChange}
                />
                <p>status:</p>
                <input
                    type="text"
                    id="status"
                    name="status"
                    value={formData.status}
                    onChange={handleChange}
                />
                <p>youtube_url:</p>
                <input
                    type="text"
                    id="youtube_url"
                    name="youtube_url"
                    value={formData.youtube_url}
                    onChange={handleChange}
                />

                <p>
                    <button type="submit">保存する</button>
                </p>
            </form>

            <button onClick={handleBack}>戻る</button>
        </div>
    );
};

export default Create
				
			

更新画面を作成する

1.ルーティングを作成します。
routes/web.phpに下記を記載してください。

				
					
Route::middleware('auth')->prefix('videos')->group(function () {
    Route::get('/update/{id}', [VideoController::class, 'edit'])->name('video.edit');
    Route::put('/update/{id}', [VideoController::class, 'update'])->name('video.update');
});
				
			
2.コントローラーを編集します。
VideoController.phpを編集します。
				
					
public function edit(string $id)
{
    $video = Video::find($id);
    return Inertia::render('Videos/Update', [
        'video' => $video
    ]);
}

public function update(Request $request, string $id)
{
    // バリデーション
    $validatedData = $request->validate([
            'title' => 'required|max:255',
            'text' => 'required',
            'status' => 'required',
            'youtube_url' => 'required|url'
    ]);

    // データの更新
    $video = Video::find($id);
    $video->update($validatedData);

    // 必要に応じてレスポンスを返す
    return redirect()->route('video.index')->with('success', 'ビデオを更新しました。');
}
				
			

3.ビューを作成します。
Update.jsx

				
					
import React, {useEffect, useState} from 'react';
import { Inertia } from '@inertiajs/inertia';
import {Link} from "@inertiajs/react";

const Update = ( { video } ) => {
    const handleBack = () => {
        window.history.back();
    };

    // フォームデータの状態管理
    const [formData, setFormData] = useState({
        title: '',
        text: '',
        status: '',
        youtube_url: ''
    });

    // 初期データの設定
    useEffect(() => {
        if (video) {
            setFormData({
                title: video.title,
                text: video.text,
                status: video.status,
                youtube_url: video.youtube_url
            });
        }
    }, );

    const handleChange = (e) => {
        setFormData({ ...formData, [e.target.name]: e.target.value });
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        Inertia.put(`/videos/update/${video.id}`, formData)
    }


    return (
        <div>
            <p>Update</p>
            <form onSubmit={handleSubmit}>
                <p>title:</p>
                <input
                    type="text"
                    id="title"
                    name="title"
                    value={formData.title}
                    onChange={handleChange}
                />
                <p>text:</p>
                <input
                    type="text"
                    id="text"
                    name="text"
                    value={formData.text}
                    onChange={handleChange}
                />
                <p>status:</p>
                <input
                    type="text"
                    id="status"
                    name="status"
                    value={formData.status}
                    onChange={handleChange}
                />
                <p>youtube_url:</p>
                <input
                    type="text"
                    id="youtube_url"
                    name="youtube_url"
                    value={formData.youtube_url}
                    onChange={handleChange}
                />

                <p>
                    <button type="submit">更新する</button>
                </p>
            </form>

            <button onClick={handleBack}>戻る</button>
        </div>
    );
};

export default Update
				
			

削除機能を作成する

1.ルーティングを作成します。
routes/web.phpに下記を記載してください。

				
					
Route::middleware('auth')->prefix('videos')->group(function () {
    Route::delete('/delete/{id}', [VideoController::class, 'destroy'])->name('video.destroy');
});
				
			
2.コントローラーを編集します。
VideoController.phpを編集します。
				
					
public function destroy(string $id)
{
    $video = Video::find($id);
    $video->delete();

    return redirect()->route('video.index')->with('success', 'ビデオを削除しました。');
}
				
			

3.ビューを作成します。
Show.jsx

				
					
import React from 'react';
import {Link} from "@inertiajs/react";
import {Inertia} from "@inertiajs/inertia";

const Video = ({ video }) => {
    const handleBack = () => {
        window.history.back();
    };

    const handleDelete = () => {
        if (window.confirm('本当に削除しますか?')) {
            Inertia.delete(`/videos/delete/${video.id}`);
        }
    }

    return (
        <div>
            <p>{video.title}</p>
            <p>{video.text}</p>
            <p>{video.status}</p>
            <p>{video.youtube_url}</p>
            <p><Link href={`/videos/update/${video.id}`}>更新</Link></p>
            <p>
                <button onClick={handleDelete}>削除</button>
            </p>
            <button onClick={handleBack}>戻る</button>
        </div>
    );
};

export default Video

				
			

Chakra UIをインストールする

				
					
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion
				
			

.tsxやindex.jsなどに作ったコンポーネントを”Chakra Provider”で囲います。

1.resources/js/Pages/Dashboard.jsxファイルに下記を記載してください。

				
					
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head } from '@inertiajs/react';
import {Box, ChakraProvider} from '@chakra-ui/react'

export default function Dashboard({ auth }) {
    return (
        <ChakraProvider>
            <div>
                <Head title="Dashboardxxxxxxx" />

                <div className="py-12">
                    <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
                        <div className="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                            <div className="p-6 text-gray-900">You're logged in!</div>
                            <p>てすと23</p>
                        </div>
                    </div>
                </div>
            </div>
        </ChakraProvider>
    );
}

				
			

【Chakura UI】Stackによるレイアウト制御

公式:https://chakra-ui.com/docs/components/stack

Index.jsxを修正します。

				
					
import React from 'react';
import {Link} from "@inertiajs/react";
import {Box, HStack, StackDivider, VStack} from "@chakra-ui/react";

const Videos = ({ videos }) => {
    return (
        <div>
            {videos.map((video, index) => (
                <HStack spacing='24px' key={index}>
                    <Box w='80px' h='40px' m={'10px'} bg='yellow.200'>
                        {video.title}
                    </Box>
                    <Box w='40px' h='40px' m={'10px'} bg='tomato'>
                        <Link href={`/videos/detail/${video.id}`}>詳細</Link>
                    </Box>
                </HStack>
            ))}

            <Link href={`/videos/create`}>新規作成</Link>

        </div>
    );
};
				
			

【Chakura UI】Tableを利用する

Index.jsxを修正します。

				
					
import React from 'react';
import {Link} from "@inertiajs/react";
import {
    Box,
    HStack,
    StackDivider,
    Table,
    TableCaption,
    TableContainer,
    Tbody,
    Td,
    Th,
    Thead,
    Tr,
    VStack
} from "@chakra-ui/react";

const Videos = ({videos}) => {
    return (
        <>
            <HStack m={'20px'}>
                <TableContainer>
                    <Table variant="simple">
                        <Thead>
                            <Tr bg={'lightskyblue'}>
                                <Th borderWidth="1px">タイトル</Th>
                                <Th borderWidth="1px">詳細</Th>
                            </Tr>
                        </Thead>
                        <Tbody>
                            {videos.map((video, index) => (
                                <Tr spacing='24px' key={index}>
                                    <Td w='80px' h='40px' m={'10px'} p={'10px'} borderWidth="1px">
                                        {video.title}
                                    </Td>
                                    <Td w='40px' h='40px' m={'10px'} p={'10px'} borderWidth="1px">
                                        <Link href={`/videos/detail/${video.id}`}>詳細</Link>
                                    </Td>
                                </Tr>
                            ))}
                        </Tbody>
                        <TableCaption>
                            <Link href={`/videos/create`}>新規作成</Link>
                        </TableCaption>
                    </Table>
                </TableContainer>
            </HStack>
        </>
    );
};

export default Videos
				
			

【Chakura UI】Textを利用する

Index.jsxを修正します。

https://chakra-ui.com/docs/components/text/usage

				
					
import React from 'react';
import {Link} from "@inertiajs/react";
import {
    Box,
    HStack,
    StackDivider,
    Table,
    TableCaption,
    TableContainer,
    Tbody,
    Td,
    Th,
    Thead,
    Tr,
    VStack,
    Text,
    ChakraProvider
} from "@chakra-ui/react";

const Videos = ({videos}) => {
    return (
        <ChakraProvider>
            <>
                <VStack align={'start'}>
                    <Text fontSize='3xl' m={'20px'}>動画一覧</Text>
                    <HStack m={'20px'}>
                        <TableContainer>
                            <Table variant="simple">
                                <Thead>
                                    <Tr bg={'lightskyblue'}>
                                        <Th borderWidth="1px">タイトル</Th>
                                        <Th borderWidth="1px">詳細</Th>
                                    </Tr>
                                </Thead>
                                <Tbody>
                                    {videos.map((video, index) => (
                                        <Tr spacing='24px' key={index}>
                                            <Td w='80px' h='40px' m={'10px'} p={'10px'} borderWidth="1px">
                                                {video.title}
                                            </Td>
                                            <Td w='40px' h='40px' m={'10px'} p={'10px'} borderWidth="1px">
                                                <Link href={`/videos/detail/${video.id}`}>詳細</Link>
                                            </Td>
                                        </Tr>
                                    ))}
                                </Tbody>
                                <TableCaption>
                                    <Link href={`/videos/create`}>新規作成</Link>
                                </TableCaption>
                            </Table>
                        </TableContainer>
                    </HStack>
                </VStack>
            </>
        </ChakraProvider>
    );
};

export default Videos
				
			

【Chakura UI】ボタンを利用する

Index.jsxを修正します。

				
					
import { Button } from "@chakra-ui/react";

<Button as={Link} href="/videos/create" colorScheme="blue">新規作成</Button>
				
			

Statusの数値を文字列に変換する

Models/Video.phpを修正します。

				
					
protected $appends = ['statusName'];

    const STATUS_NOT_STARTED = 0;
    const STATUS_PROCESSING = 1;
    const STATUS_COMPLETED = 2;
    const STATUS_PENDING = 3;

    public static $statusNames = [
        self::STATUS_NOT_STARTED => '未着手',
        self::STATUS_PROCESSING => '着手',
        self::STATUS_COMPLETED => '完了',
        self::STATUS_PENDING => '保留',
    ];

    public function getStatusNameAttribute()
    {
        return self::$statusNames[$this->status] ?? null;
    }
				
			

【Chakura UI】詳細画面をデザインする

Show.jsxを修正します。

				
					
import React from 'react';
import {Link} from "@inertiajs/react";
import {Inertia} from "@inertiajs/inertia";
import {ChakraProvider, VStack, Text, HStack, Button} from "@chakra-ui/react";

const Video = ({video}) => {
    const handleBack = () => {
        window.history.back();
    };

    const handleDelete = () => {
        if (window.confirm('本当に削除しますか?')) {
            Inertia.delete(`/videos/delete/${video.id}`);
        }
    }

    return (
        <ChakraProvider>
            <VStack align={'start'} m={'20px'}>
                <Text fontSize='3xl'>動画詳細</Text>

                <HStack w={'780px'} p={'10px'} mt={'30px'} borderBottomWidth="1px">
                    <Text w={'180px'} fontWeight={'bold'}>タイトル</Text>
                    <Text w={'600px'}>{video.title}</Text>
                </HStack>

                <HStack w={'780px'} p={'10px'} borderBottomWidth="1px">
                    <Text w={'180px'} fontWeight={'bold'}>撮影内容</Text>
                    <Text w={'600px'}>{video.text}</Text>
                </HStack>
                <HStack w={'780px'} p={'10px'} borderBottomWidth="1px">
                    <Text w={'180px'} fontWeight={'bold'}>ステータス</Text>
                    <Text w={'600px'}>{video.statusName}</Text>
                </HStack>

                <HStack w={'780px'} p={'21px'} borderBottomWidth="1px">
                    <Text w={'180px'} fontWeight={'bold'}>URL</Text>
                    <Text w={'600px'}>{video.youtube_url}</Text>
                </HStack>

                <HStack spacing={5} m={'20px'}>
                    <Text>
                        <Button as={Link} href={`/videos/update/${video.id}`} colorScheme='orange'>変更</Button>
                    </Text>
                    <Text>
                        <Button onClick={handleDelete}>削除</Button>
                    </Text>
                    <Button onClick={handleBack}>戻る</Button>
                </HStack>
            </VStack>
        </ChakraProvider>
    );
};

export default Video

				
			

【Chakura UI】編集画面をデザインする

Update.jsxを修正します。

				
					
import React, {useEffect, useState} from 'react';
import {Inertia} from '@inertiajs/inertia';
import {ChakraProvider, HStack, Text, VStack, Input, Textarea, Button, Select} from "@chakra-ui/react";


const Update = ({video, statuses}) => {
    const handleBack = () => {
        window.history.back();
    };

    // フォームデータの状態管理
    const [formData, setFormData] = useState({
        title: '',
        text: '',
        status: '',
        youtube_url: ''
    });

    // 初期データの設定
    useEffect(() => {
        if (video) {
            setFormData({
                title: video.title,
                text: video.text,
                status: video.status,
                youtube_url: video.youtube_url
            });
        }
    }, );

    const handleChange = (e) => {
        setFormData({...formData, [e.target.name]: e.target.value});
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        Inertia.put(`/videos/update/${video.id}`, formData)
    }

    return (
        <ChakraProvider>
            <VStack align={'start'} m={'20px'}>
                <Text fontSize='3xl'>動画編集</Text>
                <form onSubmit={handleSubmit}>
                    <HStack w={'780px'} p={'10px'} mt={'30px'} borderBottomWidth="1px">
                        <Text w={'180px'} fontWeight={'bold'}>タイトル</Text>
                        <Input placeholder='タイトル'
                               size='lg'
                               type="text"
                               id="title"
                               name="title"
                               value={formData.title}
                               onChange={handleChange}
                        />
                    </HStack>

                    <HStack w={'780px'} p={'10px'} borderBottomWidth="1px">
                        <Text w={'180px'} fontWeight={'bold'}>撮影内容</Text>
                        <Textarea placeholder='撮影内容'
                               id="text"
                               name="text"
                               value={formData.text}
                               onChange={handleChange}
                        />
                    </HStack>

                    <HStack w={'780px'} p={'10px'} borderBottomWidth="1px">
                        <Text w={'180px'} fontWeight={'bold'}>ステータス</Text>
                        <Select placeholder='ステータス' value={formData.status} onChange={(e) => setFormData({ ...formData, status: e.target.value })}>
                            {statuses.map((status, i) => <option key={i} value={i}>{status}</option>)}
                        </Select>
                    </HStack>

                    <HStack w={'780px'} p={'10px'} borderBottomWidth="1px">
                        <Text w={'180px'} fontWeight={'bold'}>URL</Text>
                        <Input placeholder='URL'
                               size='lg'
                               type="text"
                               id="youtube_url"
                               name="youtube_url"
                               value={formData.youtube_url}
                               onChange={handleChange}
                        />
                    </HStack>


                    <HStack m={'20px'}>
                        <Button type="submit" colorScheme='orange'>更新する</Button>
                        <Button onClick={handleBack}>戻る</Button>
                    </HStack>
                </form>
            </VStack>
        </ChakraProvider>
    );
};

export default Update

				
			

【Chakura UI】新規作成画面をデザインする

Create.jsxを修正します。

				
					
import React, {useState} from 'react';
import {Inertia} from '@inertiajs/inertia';
import {Button, ChakraProvider, HStack, Input, Select, Text, Textarea, VStack} from "@chakra-ui/react";

const Create = ({statuses}) => {
    const handleBack = () => {
        window.history.back();
    };

    // useStateはReactのフック(Hook)の一つで、関数コンポーネント内で状態(state)を持たせるために使用されます。
    // 一番右がデフォルト値
    // 左側が値、右型が値を設定するためのもの
    // なので、setFormDataに値を詰めると、formDataとしてformの値が扱える。
    const [formData, setFormData] = useState({
        title: '',
        text: '',
        status: '',
        youtube_url: ''
    });

    // name属性とvalue属性をformDataとして設定する。
    // ここのsetFormDataで、useStateのtitleとかを変更している。
    const handleChange = (e) => {
        setFormData({...formData, [e.target.name]: e.target.value});
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log(111)
        Inertia.post('/videos/store', formData)
    }


    return (
        <ChakraProvider>
            <VStack align={'start'} m={'20px'}>
                <Text fontSize='3xl'>新規作成</Text>
                <form onSubmit={handleSubmit}>
                    <HStack w={'780px'} p={'10px'} mt={'30px'} borderBottomWidth="1px">
                        <Text w={'180px'} fontWeight={'bold'}>タイトル</Text>
                        <Input placeholder='タイトル'
                               size='lg'
                               type="text"
                               id="title"
                               name="title"
                               value={formData.title}
                               onChange={handleChange}
                        />
                    </HStack>

                    <HStack w={'780px'} p={'10px'} borderBottomWidth="1px">
                        <Text w={'180px'} fontWeight={'bold'}>撮影内容</Text>
                        <Textarea placeholder='撮影内容'
                                  id="text"
                                  name="text"
                                  value={formData.text}
                                  onChange={handleChange}
                        />
                    </HStack>


                    <HStack w={'780px'} p={'10px'} borderBottomWidth="1px">
                        <Text w={'180px'} fontWeight={'bold'}>ステータス</Text>
                        <Select placeholder='ステータス' value={formData.status} onChange={(e) => setFormData({ ...formData, status: e.target.value })}>
                            {statuses.map((status, i) => <option key={i} value={i}>{status}</option>)}
                        </Select>
                    </HStack>

                    <HStack w={'780px'} p={'10px'} borderBottomWidth="1px">
                        <Text w={'180px'} fontWeight={'bold'}>URL</Text>
                        <Input placeholder='URL'
                               size='lg'
                               type="text"
                               id="youtube_url"
                               name="youtube_url"
                               value={formData.youtube_url}
                               onChange={handleChange}
                        />
                    </HStack>

                    <HStack m={'20px'}>
                        <Button type="submit" colorScheme='blue'>保存する</Button>
                        <Button onClick={handleBack}>戻る</Button>
                    </HStack>
                </form>
            </VStack>
        </ChakraProvider>
    );
};

export default Create

				
			

TypeScriptのインストール

(1)下記を見て、ts化する
https://github.com/laravel/breeze

TypescriptをLaravelにインストールします。

				
					
yarn add @types/react @types/react-dom typescript @types/node
				
			

作成したファイルをTS化する

型ファイルを作成する。

				
					
export interface VideoObject {
    id: string
    title: string
    text: string
    status: string
    youtube_url: string
    user_id: number
    statusName: string
    created_at: Date
    updated_at: Date
}

export interface VideoProps {
    video: VideoObject;
}


export interface VideosProps {
    videos: VideoObject[];
}

// video propsにstatusesを追加する
export interface StatusesBase {
    statuses: string[];
}
export interface UpdateProps extends VideoProps, StatusesBase {}

export interface StatusProps extends StatusesBase {}