■P147

────────────────
sail node -v
────────────────


■P148

●package.json
────────────────
{
    "$schema": "https://www.schemastore.org/package.json",
    "private": true,
    "type": "module",
    "scripts": {
        "build": "vite build",
        "dev": "vite"
    },
    "devDependencies": {
        "@tailwindcss/vite": "^4.0.0",
        "axios": "^1.11.0",
        "concurrently": "^9.0.1",
        "laravel-vite-plugin": "^3.0.0",
        "tailwindcss": "^4.0.0",
        "vite": "^8.0.0"
    }
}
────────────────

────────────────
sail npm install
────────────────


■P149

────────────────
sail npm run build
────────────────

────────────────
sail npm run dev
────────────────


●vite.config.js
────────────────
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
        tailwindcss(),
    ],
    server: {
        watch: {
            ignored: ['**/storage/framework/views/**'],
        },
    },
});
────────────────


■P150

────────────────
@vite(['resources/css/app.css', 'resources/js/app.js'])
────────────────

●public/build/manifest.json
────────────────
{
  "resources/css/app.css": {
    "file": "assets/app-BUNJgZf8.css",
    "name": "app",
    "names": [
      "app.css"
    ],
    "src": "resources/css/app.css",
    "isEntry": true
  },
  "resources/js/app.js": {
    "file": "assets/app-CiZEzA8M.js",
    "name": "app",
    "src": "resources/js/app.js",
    "isEntry": true
  }
}
────────────────


■P151

●resources/css/app.css
────────────────
@import 'tailwindcss';

@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
────────────────

────────────────
https://tailwindcss.com/docs/functions-and-directives
────────────────


■P152

●resources/views/components/layout.blade.php
────────────────
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    <title>{{ $title ?? 'つぶやきアプリ' }}</title>
</head>
<body class="bg-gray-100">
    {{ $slot }}
</body>
</html>
────────────────


■P153

●resources/views/tweet/index.blade.php
────────────────
<x-layout title="TOP | つぶやきアプリ">
    <h1>ここに内容が入ります。</h1>
</x-layout>
────────────────

────────────────
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0,
        maximum-scale=1.0, minimum-scale=1.0">
    …省略…
    <title>TOP | つぶやきアプリ</title>
</head>
<body class="bg-gray-100">
    <h1>ここに内容が入ります。</h1>
</body>
</html>
────────────────


■P154

────────────────
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    <title>{{ $title ?? 'つぶやきアプリ' }}</title>
</head>
<body class="bg-gray-100">
    {{ $slot }}

    <aside>{{ $aside }}</aside>
</body>
</html>
────────────────

────────────────
<x-layout title="TOP | つぶやきアプリ">
    <h1>ここに内容が入ります。</h1>
    <x-slot name="aside">追加したslot</x-slot>
</x-layout>
────────────────


■P155

●resources/views/components/form/error.blade.php
────────────────
<div class="flex items-center gap-1 mt-2 text-red-500 text-sm" role="alert">
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
        <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
    </svg>
    <span>{{ $slot }}</span>
</div>
────────────────

●resources/views/components/form/textarea.blade.php
────────────────
@props([
    'name',
    'label' => '',
    'value' => '',
    'rows' => 3,
    'required' => false,
    'autofocus' => false,
    'placeholder' => '',
    'helpText' => ''
])

<div class="mt-4">
    @if($label)
        <label for="{{ $name }}" class="block text-sm font-medium text-gray-700 mb-1">
            {{ $label }}
        </label>
    @endif
    <textarea
        id="{{ $name }}"
        name="{{ $name }}"
        rows="{{ $rows }}"
        placeholder="{{ $placeholder }}"
        {{ $required ? 'required' : '' }}
        {{ $autofocus ? 'autofocus' : '' }}
        {{ $attributes->merge(['class' => 'focus:ring-blue-400 focus:border-blue-400 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md p-2']) }}
    >{{ old($name, $value) }}</textarea>
    @if($helpText)
        <p class="mt-2 text-sm text-gray-500">{{ $helpText }}</p>
    @endif
    @error($name)
        <x-form.error>{{ $message }}</x-form.error>
    @enderror
</div>
────────────────


■P156

────────────────
<x-form.textarea
    name="tweet"
    rows="3"
    placeholder="つぶやきを入力"
    helpText="140文字まで"
    class="bg-yellow-50"
    required
/>
────────────────

────────────────
$attributes = [
    'class' => 'bg-yellow-50'
]
────────────────


■P157

────────────────
class="focus:ring-blue-400 focus:border-blue-400 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md p-2 bg-yellow-50"
────────────────

●resources/views/components/element/button.blade.php
────────────────
<button
        type="submit"
        class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
    {{ $slot }}
</button>
────────────────

●resources/views/components/tweet/form/post.blade.php
────────────────
@auth
    <div class="p-4">
        <form action="{{ route('tweet.create') }}" method="post">
            @csrf
            <x-form.textarea
                name="tweet"
                rows="3"
                placeholder="つぶやきを入力"
                helpText="140文字まで"
                required
            />
            <div class="flex flex-wrap justify-end mt-4">
                <x-element.button>
                    つぶやく
                </x-element.button>
            </div>
        </form>
    </div>
@endauth
────────────────


■P158

●resources/views/components/layout/single.blade.php
────────────────
<div class="flex justify-center">
    <div class="max-w-screen-sm w-full">
        {{ $slot }}
    </div>
</div>
────────────────

●resources/views/tweet/index.blade.php
────────────────
<x-layout title="TOP | つぶやきアプリ">
    <x-layout.single>
        <h2 class="text-center text-blue-500 text-4xl font-bold mt-8 mb-8">
            つぶやきアプリ
        </h2>
        <x-tweet.form.post></x-tweet.form.post>
    </x-layout.single>
</x-layout>
────────────────


■P159

●resources/views/components/element/button-a.blade.php
────────────────
@props([
    'href' => '',
    'theme' => 'primary',
])

@php
    $themeClasses = [
        'primary' => 'text-white bg-blue-500 hover:bg-blue-600 focus:ring-blue-500',
        'secondary' => 'text-white bg-red-500 hover:bg-red-600 focus:ring-red-500',
    ];
    
    $baseClasses = 'inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2';
@endphp

<a href="{{ $href }}" {{ $attributes
    ->merge(['class' => $baseClasses])
    ->merge(['class' => $themeClasses[$theme] ?? $themeClasses['primary']]) }}>
    {{ $slot }}
</a>
────────────────


■P160

────────────────
->merge(['class' => $themeClasses[$theme] ?? $themeClasses['primary']]
────────────────

●resources/views/components/tweet/form/post.blade.php
────────────────
@auth
    …省略…
@endauth
@guest
<div class="flex flex-wrap justify-center">
    <div class="w-1/2 p-4 flex flex-wrap justify-evenly">
        <x-element.button-a :href="route('login')">ログイン</x-element.button-a>
        <x-element.button-a :href="route('register')" theme="secondary">ユーザー登録</x-element.button-a>
    </div>
</div>
@endguest
────────────────


■P161

●resources/views/components/layout/single.blade.php
────────────────
<div class="flex justify-center">
    <div class="max-w-screen-sm w-full">
        @auth
            <form method="post" action="{{ route('logout') }}">
                @csrf
                <div class="flex justify-end p-4">
                    <button
                        class="mt-2 text-sm text-gray-500 hover:text-gray-800"
                        type="submit"
                    >ログアウト</button>
                </div>
            </form>
        @endauth
        {{ $slot }}
    </div>
</div>
────────────────


■P162

●resources/views/components/form/input.blade.php
────────────────
@props([
    'type' => 'text',
    'name',
    'label',
    'value' => '',
    'required' => false,
    'autofocus' => false,
    'placeholder' => ''
])

<div class="mt-4">
    <label for="{{ $name }}" class="block text-sm font-medium text-gray-700 mb-1">
        {{ $label }}
    </label>
    <input
        type="{{ $type }}"
        id="{{ $name }}"
        name="{{ $name }}"
        value="{{ old($name, $value) }}"
        placeholder="{{ $placeholder }}"
        {{ $required ? 'required' : '' }}
        {{ $autofocus ? 'autofocus' : '' }}
        {{ $attributes->merge(['class' => 'focus:ring-blue-400 focus:border-blue-400 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md p-2']) }}
    >
    @error($name)
        <x-form.error>{{ $message }}</x-form.error>
    @enderror
</div>
────────────────


■P163

●resources/views/components/auth/card.blade.php
────────────────
<div class="min-h-screen flex items-center justify-center">
    <div class="bg-white shadow-lg rounded-lg px-8 pt-6 pb-8 mb-4 w-full max-w-md">
        <h2 class="text-center text-blue-500 text-2xl font-bold mb-6">
            {{ $title }}
        </h2>
        {{ $slot }}
    </div>
</div>
────────────────

●resources/views/components/auth/link.blade.php
────────────────
@props([
    'href'
])

<a href="{{ $href }}" {{ $attributes->merge(['class' => 'inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800']) }}>
    {{ $slot }}
</a>
────────────────


■P164

●resources/views/auth/login.blade.php
────────────────
<x-layout title="ログイン | つぶやきアプリ">
    <x-layout.single>
        <x-auth.card title="ログイン">
            <form method="POST" action="{{ route('login') }}">
                @csrf
                <x-form.input
                    type="email"
                    name="email"
                    label="メールアドレス"
                    placeholder="メールアドレスを入力"
                    required
                    autofocus
                />
                
                <x-form.input
                    type="password"
                    name="password"
                    label="パスワード"
                    placeholder="パスワードを入力"
                    required
                />
                
                <div class="flex justify-center mt-8 mb-6">
                    <x-element.button>
                        ログイン
                    </x-element.button>
                </div>
            </form>
            <div class="text-center">
                <x-auth.link href="{{ route('register') }}">
                    アカウントをお持ちでない方はこちら
                </x-auth.link>
            </div>
        </x-auth.card>
    </x-layout.single>
</x-layout>
────────────────


■P165

●resources/views/auth/register.blade.php
────────────────
<x-layout title="ユーザー登録 | つぶやきアプリ">
    <x-layout.single>
        <x-auth.card title="ユーザー登録">
            <form method="POST" action="{{ route('register') }}">
                @csrf
                <x-form.input
                    type="text"
                    name="name"
                    label="お名前"
                    placeholder="お名前を入力"
                    required
                    autofocus
                />
                <x-form.input
                    type="email"
                    name="email"
                    label="メールアドレス"
                    placeholder="メールアドレスを入力"
                    required
                />
                <x-form.input
                    type="password"
                    name="password"
                    label="パスワード"
                    placeholder="パスワードを入力"
                    required
                />
                <x-form.input
                    type="password"
                    name="password_confirmation"
                    label="パスワード確認"
                    placeholder="パスワードを再入力"
                    required
                />
                <div class="flex justify-center mt-8 mb-6">
                    <x-element.button>
                        登録
                    </x-element.button>
                </div>
            </form>

            <div class="text-center">
                <x-auth.link href="{{ route('login') }}">
                    すでにアカウントをお持ちの方はこちら
                </x-auth.link>
            </div>
        </x-auth.card>
    </x-layout.single>
</x-layout>
────────────────


■P167

●resources/views/components/tweet/list.blade.php
────────────────
@props([
    'tweets' => []
])
<div class="bg-white rounded-md shadow-lg mt-5 mb-5">
    <ul>
        @foreach($tweets as $tweet)
        <li class="border-b last:border-b-0 border-gray-200 p-4 flex items-start justify-between">
            <div>
                <span class="inline-block rounded-full text-gray-600 bg-gray-100 px-2 py-1 text-xs mb-2">
										{{ $tweet->user->name }}
								</span>
                <p class="text-gray-600">{!! nl2br(e($tweet->content)) !!}</p>
            </div>
            <div>
                <!-- TODO 編集と削除 -->
            </div>
        </li>
        @endforeach
    </ul>
</div>
────────────────


■P168

●resources/views/tweet/index.blade.php
────────────────
<x-layout title="TOP | つぶやきアプリ">
    <x-layout.single>
        <h2 class="text-center text-blue-500 text-4xl font-bold mt-8 mb-8">
            つぶやきアプリ
        </h2>
        <x-tweet.form.post></x-tweet.form.post>
        <x-tweet.list :tweets="$tweets"></x-tweet.list>
    </x-layout.single>
</x-layout>
────────────────


■P169

────────────────
sail artisan make:component Tweet/Options
────────────────


■P170

●app/View/Components/Tweet/Options.php
────────────────
<?php

namespace App\View\Components\Tweet;

use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;

class Options extends Component
{
    /**
     * Create a new component instance.
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the view / contents that represent the component.
     */
    public function render(): View|Closure|string
    {
        return view('components.tweet.options');
    }
}
────────────────

●app/View/Components/Tweet/Options.php
────────────────
<?php

namespace App\View\Components\Tweet;

use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;

class Options extends Component
{
    public function __construct(
        private int $tweetId,
        private int $userId
    ) {
    }

    public function render(): View|Closure|string
    {
        return view('components.tweet.options')
            ->with('tweetId', $this->tweetId)
            ->with('myTweet', \Illuminate\Support\Facades\Auth::id() === $this->userId);
    }
}
────────────────


■P171

●resources/views/components/tweet/options.blade.php
────────────────
@if($myTweet)
<details class="tweet-option relative text-gray-500">
    <summary>
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
            <path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
        </svg>
    </summary>
    <div class="bg-white rounded shadow-md absolute right-0 w-24 z-20 pt-1 pb-1">
        <div>
            <a href="{{ route('tweet.update.index', ['tweetId' => $tweetId]) }}" class="flex items-center gap-1 pt-1 pb-1 pl-3 pr-3 hover:bg-gray-100">
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
                    <path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
                </svg>
                <span>編集</span>
            </a>
        </div>
        <div>
            <form action="{{ route('tweet.delete', ['tweetId' => $tweetId]) }}" method="post" onclick="return confirm('削除してもよろしいですか？');">
                @method('DELETE')
                @csrf
                <button type="submit" class="flex items-center gap-1 w-full pt-1 pb-1 pl-3 pr-3 hover:bg-gray-100">
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
                        <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
                    </svg>
                    <span>削除</span>
                </button>
            </form>
        </div>
    </div>
</details>
@endif
────────────────


■P173

●resources/views/components/tweet/options.blade.php
────────────────
@once
@push('css')
    <style>
        .tweet-option > summary {
            list-style: none;
            cursor: pointer;
        }
        .tweet-option[open] > summary::before {
            position: fixed;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
            z-index: 10;
            display: block;
            content: " ";
            background: transparent;
        }
    </style>
@endpush
@endonce
────────────────


■P174

●resources/views/components/layout.blade.php（全文）
────────────────
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    <title>{{ $title ?? 'つぶやきアプリ' }}</title>
    @stack('css')
</head>
<body class="bg-gray-100">
    {{ $slot }}
</body>
</html>
────────────────

●resources/views/components/tweet/list.blade.php
────────────────
            <div>
                <!-- TODO 編集と削除 -->
                <x-tweet.options :tweetId="$tweet->id" :userId="$tweet->user_id"></x-tweet.options>
            </div>
────────────────


■P175

●resources/views/components/tweet/form/put.blade.php
────────────────
@props([
    'tweet'
])
<div class="p-4">
    <form action="{{ route('tweet.update.put', ['tweetId' => $tweet->id]) }}" method="post">
        @method('PUT')
        @csrf
        @if (session('feedback.success'))
            <x-alert.success>{{ session('feedback.success') }}</x-alert.success>
        @endif
        <x-form.textarea
            name="tweet"
            rows="3"
            placeholder="つぶやきを入力"
            helpText="140文字まで"
            value="{{ $tweet->content }}"
            required
        />
        <div class="flex flex-wrap justify-end mt-4">
            <x-element.button>
                編集
            </x-element.button>
        </div>
    </form>
</div>
────────────────


■P176

●resources/views/components/alert/success.blade.php
────────────────
<div class="w-full mt-1 mb-2 p-2 bg-green-500 items-center text-white leading-none lg:rounded-full flex lg:inline-flex" role="alert">
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
        <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
    </svg>
    <span class="font-semibold mr-2 text-left flex-auto pl-1">{{ $slot }}</span>
</div>
────────────────

●resources/views/components/element/breadcrumbs.blade.php
────────────────
@props([
'breadcrumbs' => [
    [
        'href' => '/',
        'label' => 'TOP'
    ]
]
])
<nav class="text-black mx-4 my-3" aria-label="Breadcrumb">
    <ol class="list-none p-0 inline-flex">
        @foreach($breadcrumbs as $breadcrumb)
            @if ($loop->last)
                <li>
                    <a href="{{ $breadcrumb['href'] }}" class="text-gray-500" aria-current="page">{{ $breadcrumb['label'] }}</a>
                </li>
            @else
                <li class="flex items-center">
                    <a href="{{ $breadcrumb['href'] }}" class="hover:underline">{{ $breadcrumb['label'] }}</a>
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
                        <path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
                    </svg>
                </li>
            @endif
        @endforeach
    </ol>
</nav>
────────────────


■P177

●resources/views/tweet/update.blade.php
────────────────
<x-layout title="編集 | つぶやきアプリ">
    <x-layout.single>
        <h2 class="text-center text-blue-500 text-4xl font-bold mt-8 mb-8">
            つぶやきアプリ
        </h2>
        @php
            $breadcrumbs = [
                ['href' => route('tweet.index'), 'label' => 'TOP'],
                ['href' => '#', 'label' => '編集']
            ];
        @endphp
        <x-element.breadcrumbs :breadcrumbs="$breadcrumbs"></x-element.breadcrumbs>
        <x-tweet.form.put :tweet="$tweet"></x-tweet.form.put>
    </x-layout.single>
</x-layout>
────────────────
