改めて見ると、正直、酷かったので、6カ月たって、大幅にリニューアルしました。
またまた、rikuさんの講座から
その他リファレンスサイト
アプリの概要
・likeボタンとunlikeボタンをクリックしてuserを切り替えながらマッチング
project start
#初期設定
######1.laravelインストール
laravel new riku_laravel_tinder
######2.laravel-uiをインストール
https://github.com/laravel/ui
ベースはbootstrap4です。
composer require laravel/ui:^3.3
php artisan ui bootstrap --auth
npm install && npm run dev
npm run dev
リンクとルートが作成される
レジスター画面とログイン画面も作成される
後はusersテーブルを作成すれば認証機能は完了。
######3.databaseを作成する。
edit:
env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=riku_laravel_tinder
DB_USERNAME=root
DB_PASSWORD=
phpAdminでデーターベースを作成する。
http://localhost/phpmyadmin/index.php?route=/server/databases
データーベース名:riku_laravel_tinder
文字コード:utf-8mb4_general_ci
######4.Userテーブルを作成する
php artisan migrate
######5.Userテーブルを編集する
usersテーブルにプロフィール画像のパスを保存できるカラムを追加する。
php artisan make:migration add_column_to_users_table
edit:
database\migrations\xxx_xxxx_add_column_to_users_table.php
public function up()
{
//外部キー対策無効にしている。
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
Schema::table('users', function (Blueprint $table) {
$table->string('img_url');
});
//外部キー対策有効に戻している。
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
}
----------------
public function down()
{
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('img_url');
});
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
}
修正を反映させる。
php artisan migrate
######6.layouts.appに@stack
を追加
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
<!-- asset('css/app.css')の下に追加 -->
@stack('css')
----------------------
<!-- ●ついでに`defer`を消去して</body>の上に移動させる -->
<script src="{{ asset('js/app.js') }}"></script>
@stack('js')
</body>
######7.fontawesomeの環境設定を行う。
npm install @fortawesome/fontawesome-free --save-dev
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
@import '~@fortawesome/fontawesome-free/scss/solid';
@import '~@fortawesome/fontawesome-free/scss/regular';
@import '~@fortawesome/fontawesome-free/scss/brands';
xamppで使用する場合
//+
mix.setResourceRoot('../');
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
//+ついでに
.version()
.sourceMaps();
asset()からmix()へ変更する。
<!-- Styles -->
{{-- <link href="{{ mix('css/app.css') }}" rel="stylesheet"> --}}
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
{{-- <script src="{{ asset('js/app.js') }}"></script> --}}
<script src="{{ mix('js/app.js') }}"></script>
//+
'mix_url' => env('MIX_ASSET_URL', '/riku_laravel_tinder/public/'),
npm run dev
読み込み時、fontが一瞬拡大した場合、以下のリンクを張ると不具合が解消される。
<link rel='stylesheet' href='https://unpkg.com/@fortawesome/fontawesome-svg-core@1.2.17/styles.css' integrity='sha384-bM49M0p1PhqzW3LfkRUPZncLHInFknBRbB7S0jPGePYM+u7mLTBbwL0Pj/dQ7WqR' crossorigin='anonymous'>
他にアニメーションを使う場合CDNを追記する必要がある。
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome-animation/0.0.10/font-awesome-animation.css" type="text/css" media="all" />
導入したら、下記のサンプルコードをコピペして、使用できるかテストしてみてください。
サンプルコード
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
<!-- Styles -->
<style>
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}a{background-color:transparent}[hidden]{display:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}a{color:inherit;text-decoration:inherit}svg,video{display:block;vertical-align:middle}video{max-width:100%;height:auto}.bg-white{--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity))}.bg-gray-100{--bg-opacity:1;background-color:#f7fafc;background-color:rgba(247,250,252,var(--bg-opacity))}.border-gray-200{--border-opacity:1;border-color:#edf2f7;border-color:rgba(237,242,247,var(--border-opacity))}.border-t{border-top-width:1px}.flex{display:flex}.grid{display:grid}.hidden{display:none}.items-center{align-items:center}.justify-center{justify-content:center}.font-semibold{font-weight:600}.h-5{height:1.25rem}.h-8{height:2rem}.h-16{height:4rem}.text-sm{font-size:.875rem}.text-lg{font-size:1.125rem}.leading-7{line-height:1.75rem}.mx-auto{margin-left:auto;margin-right:auto}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.ml-2{margin-left:.5rem}.mt-4{margin-top:1rem}.ml-4{margin-left:1rem}.mt-8{margin-top:2rem}.ml-12{margin-left:3rem}.-mt-px{margin-top:-1px}.max-w-6xl{max-width:72rem}.min-h-screen{min-height:100vh}.overflow-hidden{overflow:hidden}.p-6{padding:1.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-8{padding-top:2rem}.fixed{position:fixed}.relative{position:relative}.top-0{top:0}.right-0{right:0}.shadow{box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)}.text-center{text-align:center}.text-gray-200{--text-opacity:1;color:#edf2f7;color:rgba(237,242,247,var(--text-opacity))}.text-gray-300{--text-opacity:1;color:#e2e8f0;color:rgba(226,232,240,var(--text-opacity))}.text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}.text-gray-500{--text-opacity:1;color:#a0aec0;color:rgba(160,174,192,var(--text-opacity))}.text-gray-600{--text-opacity:1;color:#718096;color:rgba(113,128,150,var(--text-opacity))}.text-gray-700{--text-opacity:1;color:#4a5568;color:rgba(74,85,104,var(--text-opacity))}.text-gray-900{--text-opacity:1;color:#1a202c;color:rgba(26,32,44,var(--text-opacity))}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.w-5{width:1.25rem}.w-8{width:2rem}.w-auto{width:auto}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}@media (min-width:640px){.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:h-20{height:5rem}.sm\:ml-0{margin-left:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width:768px){.md\:border-t-0{border-top-width:0}.md\:border-l{border-left-width:1px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:bg-gray-800{--bg-opacity:1;background-color:#2d3748;background-color:rgba(45,55,72,var(--bg-opacity))}.dark\:bg-gray-900{--bg-opacity:1;background-color:#1a202c;background-color:rgba(26,32,44,var(--bg-opacity))}.dark\:border-gray-700{--border-opacity:1;border-color:#4a5568;border-color:rgba(74,85,104,var(--border-opacity))}.dark\:text-white{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.dark\:text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}.dark\:text-gray-500{--tw-text-opacity:1;color:#6b7280;color:rgba(107,114,128,var(--tw-text-opacity))}}
</style>
<style>
body {
font-family: 'Nunito', sans-serif;
}
</style>
</head>
<body class="antialiased">
<div class="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center py-4 sm:pt-0">
@if (Route::has('login'))
<div class="hidden fixed top-0 right-0 px-6 py-4 sm:block">
@auth
<a href="{{ url('/home') }}" class="text-sm text-gray-700 dark:text-gray-500 underline">Home</a>
@else
<a href="{{ route('login') }}" class="text-sm text-gray-700 dark:text-gray-500 underline">Log in</a>
@if (Route::has('register'))
<a href="{{ route('register') }}" class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">Register</a>
@endif
@endauth
</div>
@endif
<div class="col-8 mx-auto py-5">
<i class="fas fa-stroopwafel"></i> <!-- <i> 要素を使った例 -->
<span class="fas fa-stroopwafel"></span> <!-- <span> 要素を使った例 -->
<h3><i class="fab fa-font-awesome"></i> Font Awesome</h3>
<p style="color: orange; font-size: 20px;"><i class="fab fa-firefox"></i> Firefox</p>
<p><i class="fab fa-github"></i> Github</p>
<p><i class="fab fa-linux sample"></i> sample クラスで調整</p>
<style>
.sample {
color: #3962C5;
font-size: 30px;
}
</style>
<p><i class="far fa-laugh-wink"></i> アイコンを表示</p>
<p><i class="fas fa-laugh-wink"></i> スタイルの違うアイコン</p>
<p><i class="fas fa-bicycle fa-lg"></i></p>
<p><i class="fas fa-bicycle fa-2x"></i></p>
<p><i class="fas fa-bicycle fa-4x"></i></p>
<p><i class="fas fa-bicycle my_large_icon"></i></p>
<style>
.my_large_icon {
font-size: 22px;
}
</style>
<p><i class="fab fa-apple icon_green"></i></p>
<style>
.icon_green {
color: #3FDA10;
}
</style>
<div style="font-size: 2rem;">
<div><i class="fas fa-home fa-fw" style="background:#D0EBF8"></i> Home</div>
<div><i class="fas fa-info fa-fw" style="background:#D0EBF8"></i> Info</div>
<div><i class="fas fa-book fa-fw" style="background:#D0EBF8"></i> Library</div>
<div><i class="fas fa-pencil-alt fa-fw" style="background:#D0EBF8"></i> Applications</div>
<div><i class="fas fa-cog fa-fw" style="background:#D0EBF8"></i> Settings</div>
</div>
<ul class="fa-ul">
<li><span class="fa-li"><i class="fas fa-check-square"></i></span>List icon</li>
<li><span class="fa-li"><i class="far fa-check-square"></i></span>List icon</li>
<li><span class="fa-li"><i class="fas fa-check-circle"></i></span>List icon</li>
<li><span class="fa-li"><i class="far fa-check-circle"></i></span>List icon</li>
<li><span class="fa-li"><i class="fas fa-envelope"></i></span>List icon</li>
</ul>
<div class="fa-3x">
<i class="fas fa-frog"></i>
<i class="fas fa-frog fa-rotate-90"></i>
<i class="fas fa-frog fa-rotate-180"></i>
<i class="fas fa-frog fa-rotate-270"></i>
<i class="fas fa-frog fa-flip-horizontal"></i>
<i class="fas fa-frog fa-flip-vertical"></i>
</div>
<div class="fa-2x">
<i class="fas fa-spinner fa-spin"></i>
<i class="fas fa-circle-notch fa-spin"></i>
<i class="fas fa-sync fa-spin"></i>
<i class="fas fa-cog fa-spin"></i>
<i class="fas fa-spinner fa-pulse"></i>
<i class="fas fa-stroopwafel fa-spin"></i>
<i class="far fa-snowflake fa-pulse"></i>
<i class="fas fa-frog fa-spin"></i>
</div>
<p><i class="fas fa-frog fa-2x faa-wrench animated"></i> </p>
<p><i class="fas fa-kiwi-bird fa-2x faa-wrench animated-hover"></i></p>
<p><a href="#" class="faa-parent animated-hover">
<i class="fa fa-bell faa-ring"></i> 親要素にマウスオーバー時に実行
</a></p>
<i class="fas fa-bell faa-ring animated"></i>
<i class="fas fa-envelope faa-horizontal animated"></i>
<i class="fab fa-android faa-vertical animated"></i>
<i class="fas fa-bug faa-flash animated"></i>
<i class="far fa-thumbs-up faa-bounce animated"></i>
<i class="far fa-compass faa-spin animated"></i>
<i class="fas fa-rocket faa-float animated"></i>
<i class="fas fa-heartbeat faa-pulse animated"></i>
<i class="fas fa-ship faa-shake animated"></i>
<i class="fas fa-trophy faa-tada animated"></i>
<i class="fas fa-space-shuttle faa-passing animated"></i>
<i class="fas fa-fighter-jet faa-passing-reverse animated"></i>
<i class="fas fa-bomb faa-burst animated"></i>
<div style="height: 50px;">
<i class="far fa-star faa-falling animated"></i>
</div>
<p><i class="fas fa-quote-left fa-lg fa-pull-left"></i>Lorem ipsum dolor sit amet, ... quam dicta.Lorem ipsum
dolor sit amet, ... quamLorem ipsum dolor sit amet, ... quam dictaLorem ipsum dolor sit amet, ... quam dicta
dictaLorem ipsum dolor sit amet, ... quam dicta<i class="fas fa-quote-right fa-lg fa-pull-right"></i></p>
<span class="fa-stack">
<i class="fas fa-square-full fa-stack-2x"></i>
<i class="fas fa-dove fa-stack-1x fa-inverse"></i>
</span>
<span class="fa-stack fa-2x">
<i class="far fa-square fa-stack-2x"></i>
<i class="fab fa-firefox fa-stack-1x"></i>
</span>
<span class="fa-stack fa-2x" style="color:#F1840A">
<i class="fas fa-square fa-stack-2x"></i>
<i class="fab fa-firefox fa-stack-1x fa-inverse"></i>
</span>
<span class="fa-stack fa-2x">
<i class="fas fa-camera fa-stack-1x"></i>
<i class="fas fa-ban fa-stack-2x" style="color:Tomato"></i>
</span>
<span class="fa-stack fa-3x">
<i class="fas fa-circle-notch fa-stack-2x" style="color: #EE7B7D"></i>
<i class="fas fa-globe fa-stack-1x" style="color: #4865B6"></i>
</span>
</div>
</div>
</div>
<script src="{{ mix('js/app.js') }}"></script>
</body>
</html>
fortawesomeのsvg-jsを使用する場合
npm i @fortawesome/fontawesome-svg-core @fortawesome/free-brands-svg-icons @fortawesome/free-regular-svg-icons @fortawesome/free-solid-svg-icons
try {
window.Popper = require('popper.js').default;
window.$ = window.jQuery = require('jquery');
require('bootstrap');
//+
// Font Awesome
window.fontawesome = require('@fortawesome/fontawesome-svg-core');
window.fontawesome.library.add(
require('@fortawesome/free-solid-svg-icons').fas,
require('@fortawesome/free-regular-svg-icons').far,
require('@fortawesome/free-brands-svg-icons').fab,
// require('./fonts/customize-svg-icons').fac,
);
$(function () {
fontawesome.dom.watch();
});
} catch (e) { }
npm run dev
https://www.webdesignleaves.com/pr/plugins/fontawesome_02.html
svg-jsの環境設定は以上です。
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
{{-- 読み込み時等、大きくなる場合、不具合を解消してくれます --}}
<link rel='stylesheet' href='https://unpkg.com/@fortawesome/fontawesome-svg-core@1.2.17/styles.css'
integrity='sha384-bM49M0p1PhqzW3LfkRUPZncLHInFknBRbB7S0jPGePYM+u7mLTBbwL0Pj/dQ7WqR' crossorigin='anonymous'>
{{-- アニメーションのcdn --}}
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome-animation/0.0.10/font-awesome-animation.css"
type="text/css" media="all" />
<link href="{{ asset('css/font-awesome.min.css') }}" rel="stylesheet" />
<style>
h3 {
border-bottom: 1px solid #000;
}
.bgc {
background-color: MistyRose;
}
</style>
</head>
<body>
<div class="col-8 mx-auto py-5">
<h3>回転</h3>
<div class="fa-4x">
<i class="fas fa-magic"></i>
<i class="fas fa-magic" data-fa-transform="rotate-90"></i>
<i class="fas fa-magic" data-fa-transform="rotate-180"></i>
<i class="fas fa-magic" data-fa-transform="rotate-270"></i>
<i class="fas fa-magic" data-fa-transform="rotate-25"></i>
<i class="fas fa-magic" data-fa-transform="rotate--30"></i>
</div>
<h3>移動例</h3>
<div class="fa-4x">
<i class="fas fa-magic bgc" data-fa-transform="shrink-8"></i>
<i class="fas fa-magic bgc" data-fa-transform="shrink-8 up-6"></i>
<i class="fas fa-magic bgc" data-fa-transform="shrink-8 right-6"></i>
<i class="fas fa-magic bgc" data-fa-transform="shrink-8 down-6"></i>
<i class="fas fa-magic bgc" data-fa-transform="shrink-8 left-6"></i>
</div>
<h3>通常サイズで16指定</h3>
<div class="fa-4x">
<i class="fas fa-magic bgc"></i>
<i class="fas fa-magic bgc" data-fa-transform="down-16"></i>
<i class="fas fa-magic bgc" data-fa-transfor></i>
</div>
<h3>幅統一 背景あり</h3>
<div class="fa-3x">
<i class="fab fa-amazon bgc" data-fa-transform="shrink-6" data-fa-mask="fas fa-circle"></i>
<i class="fas fa-pencil-alt bgc" data-fa-transform="shrink-10 up-.5" data-fa-mask="fas fa-comment"></i>
<i class="fab fa-facebook-f bgc" data-fa-transform="shrink-3.5 down-1.6 right-1.25" data-fa-mask="fas fa-circle"></i>
<i class="fas fa-headphones bgc" data-fa-transform="shrink-6" data-fa-mask="fas fa-square"></i>
</div>
<h3>幅統一 背景なし</h3>
<div class="fa-3x">
<i class="fab fa-amazon" data-fa-transform="shrink-6" data-fa-mask="fas fa-circle"></i>
<i class="fas fa-pencil-alt" data-fa-transform="shrink-10 up-.5" data-fa-mask="fas fa-comment"></i>
<i class="fab fa-facebook-f" data-fa-transform="shrink-3.5 down-1.6 right-1.25" data-fa-mask="fas fa-circle"></i>
<i class="fas fa-headphones " data-fa-transform="shrink-6" data-fa-mask="fas fa-square"></i>
</div>
<h3>マスク(Masking)</h3>
<span class="fa-layers fa-fw">
<i class="fas fa-circle" style="color:Tomato"></i>
<i class="fa-inverse fas fa-times" data-fa-transform="shrink-6"></i>
</span>
<span class="fa-layers fa-fw">
<i class="fas fa-bookmark"></i>
<i class="fas fa-heart" data-fa-transform="shrink-10 up-2" style="color:Tomato"></i>
</span>
<span class="fa-layers fa-fw">
<i class="fas fa-play" data-fa-transform="rotate--90 grow-2"></i>
<i class="fas fa-sun fa-inverse" data-fa-transform="shrink-10 up-2"></i>
<i class="fas fa-moon fa-inverse" data-fa-transform="shrink-11 down-4.2 left-4"></i>
<i class="fas fa-star fa-inverse" data-fa-transform="shrink-11 down-4.2 right-4"></i>
</span>
<span class="fa-layers fa-fw">
<i class="fas fa-certificate"></i>
<span class="fa-layers-text fa-inverse" data-fa-transform="shrink-11.5 rotate--30" style="font-weight:900">NEW</span>
</span>
<span class="fa-layers fa-fw">
<i class="fas fa-envelope"></i>
<span class="fa-layers-counter">1,419</span>
</span>
<span class="fa-layers fa-fw">
<i class="fas fa-envelope"></i>
<span class="fa-layers-counter fa-layers-bottom-left">1,419</span>
</span>
<p class="fa-4x"><i class="fas fa-heart" data-fa-mask="fas fa-comment"></i></p>
<div class="fa-4x">
<i class="fas fa-pencil-alt" data-fa-transform="shrink-10 up-.5" data-fa-mask="fas fa-comment"></i>
<i class="fab fa-facebook-f" data-fa-transform="shrink-3.5 down-1.6 right-1.25" data-fa-mask="fas fa-circle"></i>
<i class="fas fa-bomb" data-fa-transform="shrink-6" data-fa-mask="fas fa-square"></i>
</div>
<div class="fa-4x">
<span class="fa-layers fa-fw">
<i class="fas fa-envelope"></i>
<i class="fas fa-heart" data-fa-transform="shrink-11 up-2" style="color:Tomato"></i>
</span>
</div>
</div>
</div>
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>
@extends('layouts.app')
{{--+ layouts.app の下--}}
@push('css')
<style>
.is-invalid-label {
border: 1px solid tomato;
}
.imagePreview {
height: 200px;
max-width: 140px;
}
img{
object-fit: cover;
}
</style>
@endpush
@section('content')
{{-- +画像を送信するため、formにはenctype="multipart/form-data"属性が必要。--}}
<form method="POST" action="{{ route('register') }}" novalidate
{{-- + --}}
enctype="multipart/form-data">
@csrf
{{-- + @csrf の直下に:fileを追加 --}}
<div class="form-group text-center">
{{-- プレヴュー画面 --}}
<label
class="imagePreview mx-auto col-md-6 p-0 d-block @error('image') is-invalid-label @enderror"
for="profiel">
<img class="h-100 w-100 object-fit:cover;" src="" alt="profile画像" id="profiel_img">
</label>
{{-- エラーメッセージ--}}
@if ($errors->has('image'))
<span class="text-danger">{{ $errors->first('image') }}</span>
@endif
<div class="input-group offset-md-4 col-md-8 mt-3 text-left">
<div class="custom-file">
{{-- :file--}}
<input type="file" id="profiel" class="profiel_img" name="image"
accept=".png,.jpg,.jpeg" />
<label class="custom-file-label @error('image') is-invalid-label @enderror"
for="profiel" data-browse="参照">profiel_img</label>
</div>
{{-- 取り消しボタン--}}
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary reset">取消</button>
</div>
</div>
</div>
{{-- / --}}
@endsection
{{--+ @endsectionの下--}}
@push('js')
<script>
$(document).on('change', ':file', function() {
var files = this.files ? this.files : [];
//空なら終了
if (!files.length || !window.FileReader) return;
//ファイル名をlavelに追記
$(this).next('.custom-file-label').text(files[0].name);
//タイプがimageなら
if (/^image/.test(files[0].type)) {
// FileReader関数
var reader = new FileReader();
reader.readAsDataURL(files[0]);
reader.onloadend = function() {
//srcに画像のurl(this.result)を記入
$('#profiel_img').attr('src', this.result);
}
}
});
//ファイルの取消
$('.reset').click(function() {
$('.custom-file-label').html('ファイル選択...');
$('#profiel_img').attr('src', '');
$(':file').val(null);
})
</script>
@endpush
######2.画像を保存する
strageフォルダに画像をほぞんして、
usersテーブルのimg_urlにパスを保存する。
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\Models\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
{
use RegistersUsers;
protected $redirectTo = RouteServiceProvider::HOME;
public function __construct()
{
$this->middleware('guest');
}
/**
* 1.画像をcheckするバリデーションを追加する。
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
//+
'image' => ['required','file','mimes:jpeg,png,jpg,bmb', 'max:1080'],
]);
}
/**
* 2.画像をフォルダ(storage\app\public\images)に保存する。
* 3.img_urlに取得先パスを作成する。
* (本来的にはパスをデータベースに保存するのはセキュリティ上NG)
* (名前を保存して取り出せるようにしておく)
*/
protected function create(array $data)
{
//+ リクエストから 画像のオリジナルネームを取得
$fileName = $data['image']->getClientOriginalName();
//+ 画像をstoreAs()でサーバーに保存
//+storage\app\public\imagesの下に保存される。
// Storage::putFileAs('public/images',$data['image'],$fileName);
$data['image']->storeAs('public/images',$fileName);
//+ 保存した画像の取得パスを作成する。(publicパスからの相対パス)
$fullFilePath = 'storage/images/'.$fileName;
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
//+ 取得した画像リンクのパスをimg_urlに保存する。
'img_url' => $fullFilePath,
]);
}
}
######3.(シンボリック)リンクを作成する。
保存した画像をpublicからの相対パスで取得できるようになる。
php artisan storage:link
######4.ホワイトリストを登録する
protected $fillable = [
'name',
'email',
'password',
//+
'img_url',
];
ユーザー登録を完了させ、
画像が保存されていること、
usersテーブルのimg_urlにパスが保存
されているのを確認する。
usersテーブルにuserが作成されimg_urlにpublicからの相対パスが入っている。
ストレージに画像が保存され
publicにリンクが作成されている。
#ルートを作成する。
<?php
use Illuminate\Support\Facades\Route;
//+
use App\Http\Controllers\UserController;
Route::get('/welcome', function () {
return view('welcome');
});
Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
Route::get('/', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
//middleware(['auth'])
Route::middleware(['auth'])->group(function () {
//メイン画面 相手を表示しlikes or unlikes ボタンを設置する。
Route::get('/users', [UserController::class, 'index'])->name('users.index');
//likes or unlikesメソッドをswipesに保存するメソッド
Route::post('/users/{user}', [UserController::class, 'store'])->name('users.store');
//マッチしたユーザー一覧を表示する。
Route::get('/users/matches', [UserController::class, 'matches'])->name('users.matches');
// users.show(マッチングしたユーザーのプロフィールを表示するルート)
Route::get('/users/matches/{num}', [UserController::class, 'matches_show'])->name('users.matches_show');
});
#index画面を作成する
ルート:Route::get('/users',[UserController::class,'index'])->name('users.index');
######1.ナビバーを編集する。
3つのロゴリンクを追加する。
<style>
.nav-link i{
color: grey;
font-size: 30px;
}
</style>
---------------
{{-- navを丸ごと上書き --}}
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
<div class="container">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
@guest
<li class="nav-item">
{{-- Tinder Logoでindex画面のリンクになります。 --}}
<a class="navbar-brand" href="{{ url('/welcome') }}">
welcome
</a>
</li>
@endif
</ul>
@auth
<ul class="navbar-nav mx-auto align-items-center justify-content-around flex-grow-1">
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link" href="#" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false" v-pre>
<i class="fa fa-cog fa-2x" aria-hidden="true"></i>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ url('/') }}">
top
</a>
<a class="dropdown-item" href="{{ route('logout') }}" onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
{{ __('Logout') }}
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST"
class="d-none">
@csrf
</form>
</div>
</li>
<li class="nav-item">
{{-- Tinder Logoでindex画面のリンクになります。 --}}
<a class="navbar-brand" href="{{ url('/users') }}">
<img src="https://worldvectorlogo.com/logos/tinder-1.svg" alt="Tinder Logo"
title="Tinder Logo" style="width: 100px">
</a>
</li>
{{-- commentsのアイコンですが、マッチしたusers一覧画面になります。 --}}
<li class="nav-item">
<a class="nav-link" href="{{ route('users.matches') }}">
<i class="fa fa-comments fa-2x" aria-hidden="true"></i>
</a>
</li>
</ul>
@endauth
<ul class="navbar-nav ml-auto">
@guest
@if (Route::has('login'))
<li class="nav-item">
<a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
</li>
@endif
@if (Route::has('register'))
<li class="nav-item">
<a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
</li>
@endif
@endif
</ul>
</div>
</div>
</nav>
{{-- /nav --}}
######2.ログイン後の遷移先を変更する。
home画面
の uriを/home
-> /
に変更しているため修正する。
public const HOME = '/';
編集後 loginしてhome画面にいって、
navbarにfortawesomeが表示されていればOKです。
login後url('/')のhome画面が表示されfortawesomeも表示されている。
######3.同一テーブルの多対多について
######4.swipesテーブルを作成する。
usersテーブルとusersテーブルをリレーションさせる中間テーブル
中間テーブルであるため、モデルは必要ありません。
そのための、メソッドが用意されています。
php artisan make:migration swipes
edit:
database\migrations\xxxx_create_swipes_table.php
public function up()
{
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
Schema::create('swipes', function (Blueprint $table) {
$table->id();
$table->foreignId('from_user_id')
->constrained('users')
->onUpdate('cascade')
->onDelete('cascade');
$table->foreignId('to_user_id')
->constrained('users','id')
->onUpdate('cascade')
->onDelete('cascade');
$table->boolean('is_like');
$table->timestamps();
});
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
}
public function down()
{
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
Schema::dropIfExists('swipes');
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
}
php artisan migrate
######5.usersテーブルどうしのリレーション
//+
use Illuminate\Support\Facades\Auth;
//laravelの命名規則では中間テーブルはusers_usersだが、
//今回はswipesとしているため、テーブル名を明示する必要がある。
//まだ、中間テーブルの外部キーも命名規則なら、本来ならuser_idだが、
//今回は、from_user_idとto_user_idのため、明示する必要がある。
//3番目の引数は、関係を定義しているモデルの外部キー名
//4番目の引数は、関連付けるモデルの外部キー名
public function from_users()
{
return $this->belongsToMany(self::class, "swipes", "from_user_id", "to_user_id")->withTimestamps();
}
public function to_users()
{
return $this->belongsToMany(self::class, "swipes", "to_user_id", "from_user_id")->withTimestamps();
}
↓↓↓↓↓↓↓ あなたの記事の内容
───────
// //1対1:勘違いしていました。
//https://qiita.com/kennsukea/items/3d29bf838f7f43ba1e4a
// public function swipe()
// {
// return $this->hasOne(Swipe::class,'id','from_user_id');
// }
//中間テーブルのカラムを使った関係のフィルタリングというのも使用してみる。
//https://readouble.com/laravel/8.x/ja/eloquent-relationships.html
//正直、こういう処理はどうかと思ったけど、1度やってみる。
//こうやって書き換えて、ハードルを飛び越えていける。rikuさんに感謝。
//https://github.com/alexeymezenin/laravel-best-practices/blob/master/japanese.md
//リレーション時、自分のことが好きか嫌いかを引数にしてユーザーを抽出している。
public function is_like($filter)
{
return $this->belongsToMany(self::class, "swipes", "from_user_id", "to_user_id")->wherePivot('is_like', $filter)->wherePivot('to_user_id', Auth::id())->withTimestamps();
}
//リレーション時、自分とマッチしたユーザーを抽出している。whereHasは回避した。
//と思ったら、https://qiita.com/ma7ma7pipipi/items/917eccd7f4f54a72a476
//こんなのがあった。技術は日進月歩だ。
public function matches()
{
$ids = $this->to_users()->where('is_like', true)->pluck('from_user_id');
return $this->belongsToMany(self::class, "swipes", "from_user_id", "to_user_id")->wherePivot('is_like', true)->wherePivotIn('to_user_id', $ids);
}
↑↑↑↑↑↑↑ 編集リクエストの内容
######6.疑似データを追加する。
$l_ $s_のどちらかを選択したら実行してください。
$l_はデータ量が1000超えますが時間はかかりません。
DatabaseSeederコード
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use App\Models\User;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
$l_img_urls = [
'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80%20750w',
'https://images.unsplash.com/photo-1589571894960-20bbe2828d0a?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=350&q=80',
'https://images.unsplash.com/photo-1552058544-f2b08422138a?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=350&q=80',
'https://images.unsplash.com/photo-1491349174775-aaafddd81942?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=350&q=80 334w',
'https://images.unsplash.com/photo-1544005313-94ddf0286df2?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=350&q=80 334w',
'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8cGVyc29ufGVufDB8fDB8fA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1573140247632-f8fd74997d5c?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MjB8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1599566150163-29194dcaad36?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MzF8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1596215143922-eedeaba0d91c?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MzB8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1569124589354-615739ae007b?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MzN8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mzh8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1598411072028-c4642d98352c?ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mzd8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1615813967515-e1838c1c5116?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NDJ8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1597223557154-721c1cecc4b0?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NDR8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1605405748313-a416a1b84491?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NDB8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NDV8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NDZ8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1495366691023-cc4eadcc2d7e?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NDh8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1607346256330-dee7af15f7c5?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NTJ8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NTN8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1529626455594-4ff0802cfb7e?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NTB8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NTh8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1563620915-8478239e9aab?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NTl8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1592124549776-a7f0cc973b24?ixid=MnwxMjA3fDB8MHxzZWFyY2h8Njh8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1552374196-c4e7ffc6e126?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NzR8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?ixid=MnwxMjA3fDB8MHxzZWFyY2h8Nzl8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1599256621730-535171e28e50?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NzV8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1539698103494-a76dd0436fbc?ixid=MnwxMjA3fDB8MHxzZWFyY2h8NzJ8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1508214751196-bcfd4ca60f91?ixid=MnwxMjA3fDB8MHxzZWFyY2h8ODF8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1586459023690-8bda067ff1bf?ixid=MnwxMjA3fDB8MHxzZWFyY2h8ODh8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1541647376583-8934aaf3448a?ixid=MnwxMjA3fDB8MHxzZWFyY2h8ODd8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1600481453173-55f6a844a4ea?ixid=MnwxMjA3fDB8MHxzZWFyY2h8OTN8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1541260894924-7ff059b93d54?ixid=MnwxMjA3fDB8MHxzZWFyY2h8OTZ8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1531123897727-8f129e1688ce?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTAyfHxwZXJzb258ZW58MHx8MHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1559750965-99605627de70?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTAwfHxwZXJzb258ZW58MHx8MHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1596815074853-84c4949c5b58?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTA2fHxwZXJzb258ZW58MHx8MHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1620952104324-3bef38e34a93?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTExfHxwZXJzb258ZW58MHx8MHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1563584316028-2b70b3a3a8bf?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTEwfHxwZXJzb258ZW58MHx8MHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
];
$s_img_urls = [
'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80%20750w',
'https://images.unsplash.com/photo-1589571894960-20bbe2828d0a?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=350&q=80',
'https://images.unsplash.com/photo-1552058544-f2b08422138a?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=350&q=80',
'https://images.unsplash.com/photo-1491349174775-aaafddd81942?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=350&q=80 334w',
'https://images.unsplash.com/photo-1544005313-94ddf0286df2?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=350&q=80 334w',
'https://images.unsplash.com/photo-1554151228-14d9def656e4?ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8cGVyc29ufGVufDB8fDB8fA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
'https://images.unsplash.com/photo-1573140247632-f8fd74997d5c?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MjB8fHBlcnNvbnxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60',
];
$img_urls = $l_img_urls; /*($l_img_urls or $s_img_urls)*/
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
DB::table('users')->truncate();
DB::table('swipes')->truncate();
User::factory()
->count(count($img_urls))
//$sequence()メソッドがメチャクチャいい。前はstaticで$i++とかしていた。
//連続番号がほしい時はsequence()を思い出してほしい。
->sequence(fn ($sequence) => ['img_url' => $img_urls[$sequence->index], 'email' => "sample{$sequence->index}@test.com", 'password' => '$2y$10$nm92/Dc98X3nSLcO1f4o5u28XKWxH6/7rVDs.fWDEGAuzrqQXHyhi'])
->create();
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
$likes = [];
$unlikes = [];
User::whereNotIn('id', [1,2])->each(function (User $user) use ($likes, $unlikes) {
$user_ids = User::where('id','<>',$user->id)->pluck('id')->toArray();
shuffle($user_ids);
$likes = array_slice($user_ids, 0, rand(0, count($user_ids)));
$unlikes/*差集合*/ = array_diff($user_ids, $likes);
$user->from_users()->attach($likes, ['is_like' => true]);
$user->from_users()->attach($unlikes, ['is_like' => false]);
});
}
}
php artisan db:seed
usesテーブルにデータが入力されているのを確認してください。
######7.マッチング相手を表示する。
ルート:Route::get('/users', [UserController::class, 'index'])->name('users.index');
@extends('layouts.app')
@push('css')
<style>
.tno i{
color: tomato;
font-size: 80px;
}
.tyes i{
/* color: #5de19c; */
font-size: 80px;
color: #5de19c;
}
</style>
@endpush
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card" style="min-height: 800px">
{{-- マッチしましたとフラッシュメッセージで表示 --}}
@if (session('flash_message'))
<div class="flash_message bg-success text-center py-3 my-0">
{{ session('flash_message') }}
</div>
@endif
<div class="card-header bg-white">
{{-- if文でuserが存在する時としない時で表示を区別 --}}
@if (is_null($user))
<p class="text-center my-auto">あなたの周りにユーザーはいません。</p>
@else
名前:{{ $user->name }}
@endif
</div>
<div class="card-body">
{{-- 指定する論理式がtrueかfalseと評価された場合に、ビューを@includeする。 --}}
@includeWhen(isset($user), 'users.matching_form')
</div>
</div>
</div>
</div>
</div>
@endsection
views\users\matching_form.blade.phpを作成する。
@push('css')
<style>
.tno i{
color: tomato;
font-size: 80px;
}
.tyes i{
/* color: #5de19c; */
font-size: 80px;
color: #5de19c;
}
</style>
@endpush
<div class="img border rounded mx-auto text-center py-2" style="">
{{-- ユーザーの画像 --}}
<img src="{{ asset($user->img_url) }}" alt="" class=""
style="width:350px;height:400px;object-fit:cover;object-position:center;">
</div>
<div class="row mt-1 justify-content-center align-items-center text-center" style="height:200px">
{{-- 緑のボタン --}}
<div class="col-6">
<form action="{{ route('users.store', $user) }}" method="POST">
@csrf
<input type="hidden" name="to_user_id" value="{{ $user->id }}">
<input type="hidden" name="from_user_id" value="{{ Auth::id() }}">
<input type="hidden" name="is_like" value="0">
<button class="tno rounded-circle bg-white p-3 px-4" type="submit">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</form>
</div>
{{-- 赤のボタン --}}
<div class="col-6">
<form action="{{ route('users.store', $user) }}" method="POST">
@csrf
<input type="hidden" name="to_user_id" value="{{ $user->id }}">
<input type="hidden" name="from_user_id" value="{{ Auth::id() }}">
<input type="hidden" name="is_like" value="1">
<button class="tyes rounded-circle bg-white p-3" type="submit">
<i class="fa fa-heart" aria-hidden="true"></i>
</button>
</form>
</div>
</div>
php artisan make:controller UserController
UserControllerを作成する。
use App\Models\User;
use App\Models\Swipe;
use Illuminate\Support\Facades\Auth;
-------
/**
* ●index()メソッドを作成する
* 1.ユーザーを取得してメイン画面を表示させる。
* 2.ユーザーは自分以外と、未選択のユーザーのみ取得する。
*/
public function index()
{
//ログインユーザーの取得
$auth = User::find(Auth::id());
//ログインユーザーがすでに選択したユーザー達のidを配列で取得
//to_user_id一覧が取得できる。
$swipedUserIds = $auth->from_users()->get()->pluck('id');
//idsには含まれていないusersを取得して、その一番最初のuserを取得
$user = User::where('id', '<>', $auth->id)->whereNotIn('id', $swipedUserIds)->first();
return view('users.index', compact('user'));
}
loginMail: sample0@test.com (sample1@test.com)
password: Password
でログインして、
ログイン後、Tinderのロゴをクリックして、メイン画面へ。
######8.マッチング相手を登録して切り替える。
好きか嫌いかのbtnをクリックして相手を切り替えていきます。
ルート:Route::post('/users/{user}', [UserController::class, 'store'])->name('users.store');
/**
* ●store()メソッドを作成する
* 1.好きボタンを押して、かつ、相手のis_likeの状態もtrueなら、
* マッチしましたというフラッシュメッセージ付きで
* route('users.index')にリダイレクトする。
* 2.falseなら、そのまま、route('users.index')にリダイレクトする。
* 結果index()で未選択のユーザーが取得されindex.blade.phpで表示されます。
*/
public function store(Request $request, User $user)
{
$swipes = $user->from_users();
//多対多におけるupdateOrCreate()の変わり。
$swipes->syncWithoutDetaching([$request->except('_token')]);
$is_like_auth = $swipes->wherePivot('to_user_id', Auth::id())->wherePivot('is_like', true)->exists();
//authが好きなら
if ($request->is_like) {
//その相手のユーザーのis_likeもtrueなら
if ($is_like_auth) {
//true同士なら、フラッシュメッセージをつけてメイン画面にリターン
return redirect()->route('users.index')->with('flash_message', 'マッチしました');
}
}
//falseなら、そのままメイン画面にリターン
return redirect()->route('users.index');
}
btnを押してuserが切り替わることとswipesテーブルを確認する。
userが切り替わり、マッチしたらアラートが表示される。
swipesテーブルに新たにデータが追加されている。
メモ:wherePivotとwhereの違い
$swipes->wherePivot('to_user_id', Auth::id())->wherePivot('is_like', true)->dd();
$swipes->where('to_user_id', Auth::id())->where('is_like', true)->dd();
#マッチしたユーザー一覧を表示させる。
ルート: Route::get('/users/matches', [UserController::class, 'matches'])->name('users.matches');
views
\users\matches.blade.phpを作成する。
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Dashboard') }}</div>
<div class="card-body">
<div class="row">
@foreach ($users as $key => $user)
<div class="col-12 mb-3">
<img src="{{ asset($user->img_url) }}" class="rounded-circle" height="70px" width="70px" alt="">
<a href="{{ route('users.matches_show',$key) }}" class="ml-3" style="font-size:16px;">
{{ $user->name }}
</a>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
usersコントローラーでmatches()を追加する
//+
/**
*マッチした画面一覧へ
*/
public function matches()
{
$auth = User::find(Auth::id());
//matches()はリレーションのメソッドです。
$users = $auth->matches()->orderBy('id','asc')->get();
return view('users.matches', compact('users'));
}
リレーションによるフィルタリングメソッドmatches()を追加する
public function matches()
{
$ids = $this->to_users()->where('is_like', true)->pluck('from_user_id');
return $this->from_users()->wherePivot('is_like', true)->wherePivotIn('to_user_id', $ids);
}
nabvarのコメントロゴをクリックすると、マッチしたユーザー一覧が表示されるのを確認してください。
#マッチしたユーザーのを詳細ページ。
ルート:Route::get('/users/matches/{num}', [UserController::class, 'matches_show'])->name('users.matches_show');
views
\users\matches_show.blade.phpの作成
@extends('layouts.app')
@section('content')
<div class="container col-md-8">
<div class="row justify-content-center">
<div class="card text-center" style="">
<img src="{{ asset($main_user->img_url) }}" class="mx-auto" alt="{{ $main_user->name }}"
style="height:400px;width:300px;object-fit:cover;object-position:center;">
<div class="card-body">
<h5 class="card-title">{{ $main_user->name }}</h5>
<p class="card-text">{{ $main_user->email }}</p>
<div class="d-flex flex-column">
<div class="d-flex justify-content-center flex-wrap order-md-2">
@foreach ($match_users as $key => $user)
<a class="btn border-primary mb-2 @if ($num == $key) btn-primary @endif"
href="{{ route('users.matches_show', $key) }}">{{ $key + 1 }}</a>
@endforeach
</div>
<a class="btn btn-primary mb-2 d-flex flex-column justify-content-center order-md-1"
href="{{ route('users.matches_show', $prev) }}">前</a>
<a class="btn btn-primary mb-2 order-md-3" href="{{ route('users.matches_show', $next) }}">次へ</a>
</div>
<div class="mt-3">
<a href="{{ route('users.room', $main_user->id) }}" class="btn btn-primary">連絡をとる</a>
<a href="{{ route('users.matches') }}" class="btn btn-primary">戻る</a>
</div>
</div>
</div>
</div>
</div>
@endsection
matches_show()の追加
//追記 1人のマッチング相手の詳細ページ
// +その他のマッチング相手はページネーションでリンクしている。
public function matches_show($num)
{
$auth = User::find(Auth::id());
$match_users = $auth->matches()->orderBy('id', 'asc')->get()->collect();
$main_user = $match_users[$num];
$count = $match_users->count();
$prev = $num - 1 < 0 ? $num : $num - 1;
$next = $num + 1 > $count - 1 ? $num : $num + 1;
return view('users.matches_show', compact('match_users', 'main_user', 'prev', 'next', 'num'));
}
チャット画面へのリンクを張っているため、ルートを追加する。
//middleware(['auth'])
Route::middleware(['auth'])->group(function () {
//+
//users.room(マッチングしたユーザーとチャットするルート)
Route::get('/users/room/{user}', [UserController::class, 'room'])->name('users.room');
});
memo:検索関数を使わずに$main_userを取得しています。show()とpaginationを組み合わせています。
public function matches()
{
$users = $auth->matches()->orderBy('id', 'asc')->get()->dd();
}
matches()の$usersのkeyを$numにしてgetリクエストしている。普通は$user->idを渡すが、show()+paginationで工夫しました。
matches.show()で$numで$match_usersの中から$main_userを取得している。
$match_users = $auth->matches()->orderBy('id', 'asc')->get()->collect()->dd();
matches()の$usersのkeyとusersが一致しているため。
#チャット機能
######1.Chatsモデルとテーブルを作成する
--相手とのメッセージを保存するモデルとテーブル。----
php artisan make:model Chat -a
Schema::create('chats', function (Blueprint $table) {
$table->id();
$table->foreignId('from_user_id')
->constrained('users')
->onUpdate('cascade')
->onDelete('cascade');
$table->foreignId('to_user_id')
->constrained('users','id')
->onUpdate('cascade')
->onDelete('cascade');
$table->text('message');
$table->timestamps();
});
php artisan migrate
######2.ホワイトリストを登録する。
//+
protected $fillable = [
'message',
'to_user_id',
'from_user_id',
];
######3.リレーションを作成する
本来的にはswipesと同じ同一テーブルの多対多だが、
多対多は1対多に分解して取得できる。そのため、双方向のリレーションをするために、
モデルを作成してリレーションする必要がある。
普通は同一テーブルの多対多は分解してリレーションすると思う。
//+外部キーと主キーでリレーションする。
public function chats()
{
return $this->hasMany(Chat::class, 'from_user_id', 'id');
}
#ルートを設定する。
Route::middleware(['auth'])->group(function () {
//users.room(マッチングしたユーザーとチャットするルート)
Route::get('/users/room/{user}', [UserController::class, 'room'])->name('users.room');
//+
//ユーザーとの個人メッセージの取得メソッド
Route::get('/users/room/{user}/messages', [UserController::class, 'get_messages'])->name('get_messages');
//ユーザーとの個人メッセージの保存・イベントの発火メソッド
Route::post('/users/room/{user}/store', [UserController::class, 'store_message'])->name('store_message');
#chat画面を作成する。
viewの作成
ルート:Route::get('/users/room/{user}', [UserController::class, 'room'])->name('users.room');
@extends('layouts.app')
@push('css')
<link rel="stylesheet" href="{{ asset('css/room.css') }}">
@endpush
@section('content')
<div class="container-fluid h-100">
<div class="row justify-content-center h-100">
<div class="chat"style="width:80vw;min-width:300px;max-width:550px;">
<div class="card">
<div class="card-header msg_head bg-secondary">
<div class="d-flex bd-highlight">
<div class="img_cont">
<img src="{{ asset($user->img_url) }}" class="rounded-circle user_img">
</div>
<div class="user_info">
<span>{{ $user->name }}</span>
<p>message count : {{ $user->get_room_messages_count }}</p>
</div>
</div>
</div>
<div class="card-body msg_card_body">
</div>
<div class="card-footer bg-secondary">
<div class="input-group">
<div class="input-group-append">
<span class="input-group-text attach_btn"><i class="fas fa-paperclip"></i></span>
</div>
<textarea id="message" name="message" class="form-control type_msg"
placeholder="Type your message..."></textarea>
<div class="input-group-append">
<span class="input-group-text send_btn"><i class="fas fa-location-arrow"></i></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('js')
<script>
</script>
@endpush
cssをresources\css\
に作成する
resources
\css\room.cssを作成する。
.card {
min-height: 600px;
max-height: 600px;
border-radius: 15px !important;
}
.msg_card_body {
overflow-y: scroll;
}
.card-header {
border-radius: 15px 15px 0 0 !important;
border-bottom: 0 !important;
}
.card-footer {
border-radius: 0 0 15px 15px !important;
border-top: 0 !important;
}
.attach_btn {
border-radius: 15px 0 0 15px !important;
background-color: rgba(0, 0, 0, 0.3) !important;
border: 0 !important;
color: white !important;
cursor: pointer;
}
.send_btn {
border-radius: 0 15px 15px 0 !important;
background-color: rgba(0, 0, 0, 0.3) !important;
border: 0 !important;
color: white !important;
cursor: pointer;
}
.active {
background-color: rgba(0, 0, 0, 0.3);
}
.user_img {
height: 70px;
width: 70px;
border: 1.5px solid #f5f6fa;
}
.user_img_msg {
height: 40px;
width: 40px;
border: 1.5px solid #f5f6fa;
}
.user_info {
margin-top: auto;
margin-bottom: auto;
margin-left: 15px;
}
.user_info span {
font-size: 20px;
color: white;
}
.user_info p {
font-size: 10px;
color: rgba(255, 255, 255, 0.6);
}
.msg_cotainer {
margin-top: auto;
margin-bottom: auto;
margin-left: 10px;
border-radius: 25px;
background-color: #82ccdd;
padding: 10px;
position: relative;
color: white;
}
.msg_cotainer_send {
margin-top: auto;
margin-bottom: auto;
margin-right: 10px;
border-radius: 25px;
background-color: #78e08f;
padding: 10px;
position: relative;
color: white;
}
.msg_time {
position: absolute;
left: 0;
bottom: -18px;
font-size: 10px;
color: #82ccdd;
}
.msg_time_send {
position: absolute;
right: 0;
bottom: -15px;
color: #78e08f;
font-size: 10px;
}
.msg_head {
position: relative;
}
#action_menu_btn {
position: absolute;
right: 10px;
top: 10px;
color: white;
cursor: pointer;
font-size: 20px;
}
.action_menu {
z-index: 1;
position: absolute;
padding: 15px 0;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 15px;
top: 30px;
right: 15px;
display: none;
}
.action_menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.action_menu ul li {
width: 100%;
padding: 10px 15px;
margin-bottom: 5px;
}
.action_menu ul li a {
text-decoration: none;
color: white;
}
.action_menu ul li i {
padding-right: 10px;
}
.action_menu ul li:hover {
cursor: pointer;
background-color: rgba(0, 0, 0, 0.9);
}
img {
height: 100%;
width: 100%;
object-fit: cover;
}
.msg_cotainer_send, .msg_cotainer{
overflow-wrap:normal;
word-break:break-all;
}
@media(max-width: 576px) {
.contacts_card {
margin-bottom: 15px !important;
}
}
webpack.mix.jsを修正する
const mix = require('laravel-mix');
mix.setResourceRoot('../');
mix.js('resources/js/app.js', 'public/js')
//+
.css('resources/css/room.css', 'public/css')
.sass('resources/sass/app.scss', 'public/css')
.version()
.sourceMaps();
npm run dev
本番環境では、
nmp run production
public\css\room.cssに圧縮してくれる。
コントローラーのroom()を作成する。
//+
use App\Models\Chat;
----------------------------------------------------
//+
/**
* マッチしたユーザーのルーム画面
*/
public function room(User $user)
{
$is_match_user=$user->to_users()->where('from_user_id',Auth::id())->exists();
//$auth = User::find(Auth::id());
// $is_match_user = $auth->matches()->get()->pluck('id')->contains($user->id);
if ($is_match_user) {
//get_room_messages()はリレーションのメソッドです。
//loadCountでリレーション先の個数もリレーションで取得できる。
$user = $user->loadCount('get_room_messages');
return view('users.room', compact('user'));
}
return redirect()->route('users.matches');
}
リレーションのフィルタリング関数を追加する。
public function get_room_messages()
{
//userがauthにauthがuserに送ったメッセージを取得する必要がある。
return $this->chats()->where('to_user_id', Auth::id())->orWhere(function ($q) {
$q->where('from_user_id', Auth::id())->where('to_user_id', $this->id);
});
}
チャットの表示画面を確認してください。
$user = $user->loadCount('get_room_messages');
dd($user);
#messageを送信して保存する。
ルート:Route::post('/users/room/{user}/store', [UserController::class, 'store_message'])->name('store_message');
######1.messageをajax送信する。
</div>
@endsection
//+
@push('js')
<script>
//+
$(document).ready(function() {
$('.send_btn').on('click', function() {
message = $('#message').val()
send_message(message);
});
//post送信するためcsrfが必須。
function send_message(message) {
if (!message) {
return false;
}
create_message = auth_message(message, msg_time)
$('.card-body').append(create_message);
$('#message').val('');
$.ajax({
type: 'post',
datatype: 'json',
url: "{{ route('store_message', $user) }}",
timeout: 3000,
data: {
'message': message
}, //送信するデータを指定
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
});
}
});
//htmlを作成する
new Date().toLocaleString({ timeZone: 'Asia/Tokyo' })
Date.prototype.yyyymmddhis = function() {
var yyyy = this.getFullYear().toString();
var mm = (this.getMonth() + 1).toString(); // getMonth() is zero-based
var dd = this.getDate().toString();
var h = this.getHours().toString();
var m = this.getMinutes().toString();
var s = this.getSeconds().toString();
return yyyy + "/" + (mm[1] ? mm : "0" + mm[0]) + "/" + (dd[1] ? dd : "0" + dd[0]) +
" " + (h[1] ? h : "0" + h[0]) + ":" + (m[1] ? m : "0" + m[0]) + ":" + (s[1] ? s : "0" + s[
0]); // padding
};
msg_time = new Date();
msg_time = msg_time.yyyymmddhis()
auth_img_url = "{{ Auth::user()->img_url }}"
function auth_message(message, msg_time) {
if (!message) {
return false;
}
create_message = `<div class="d-flex justify-content-end mb-4">`;
create_message += '<div class="msg_cotainer_send">';
create_message += `${message.replace(/\n/g, '<br>')}`;
create_message += `<span class="msg_time_send text-nowrap">${msg_time}</span>`;
create_message += '</div>';
create_message += '<div class="img_cont_msg">';
create_message += `<img src="${auth_img_url}" class="rounded-circle user_img_msg"></div></div>`;
return create_message;
}
</script>
@endpush
######2.送られてきmessageをDBに保存する。
/**
* messageを保存する
*/
public function store_message(Request $request, User $user)
{
$message = $request->message;
$chat = Chat::create(['message' => $message, 'from_user_id' => Auth::id(), 'to_user_id' => $user->id]);
return 'success';
}
入力されたmessageはすぐにappendして、formからmessageを消去している。
そのため、リロードすると、messageが消去されます。
DBのchatsにmessageが保存されていればOKです。
#messageを取得する
ルート:Route::get('/users/room/{user}/messages', [UserController::class, 'get_messages'])->name('get_messages');
//ajaxでメッセージを取得
//+
getMessages()
//+
function getMessages() {
$.ajax({
type: 'get',
datatype: 'json',
url: "{{ route('get_messages', $user) }}",
timeout: 3000,
})
.done(function(data, textStatus, jqXHR) {
auth_id = {{ Auth::id() }}
$result = $(".card-body"),
messages = [];
$.each(data, function(index, value) {
message = ``;
if (auth_id == value.from_user_id) {
message += auth_message(value.message, value.created_at);
} else {
message += user_message(value.message, value.created_at);
}
messages.push(message);
});
$result[0].innerHTML = messages.join("");
});
}
//htmlの作成
//+
user_img_url = "{{ $user->img_url }}";
//+
function user_message(message, msg_time) {
create_message = `<div class="d-flex justify-content-start mb-4">`;
create_message += '<div class="img_cont_msg">';
create_message += `<img src="${user_img_url}" class="rounded-circle user_img_msg"></div>`;
create_message += '<div class="msg_cotainer">';
create_message += `${message.replace(/\n/g, '<br>')}`;
create_message += `<span class="msg_time text-nowrap">${msg_time}</span>`;
create_message += '</div></div>';
return create_message;
}
コントローラーにget_messages()を追記
//+
/**
* messageを取得
*/
public function get_messages(User $user)
{
$messages = $user->get_room_messages()->get();
return $messages;
}
#リアルタイムチャットを実装する
messageを送信しても通信側が自発的に取得(リロード等)しない限り、
messageを取得できない。
リロードすると、相手のmessageを取得できる状態。
これを、送信と同時に相手側で受信できる機能を作成します。
######1.Pusher Channels PHP SDKをインストールと環境構築
導入リファレンスサイト
公式サイト
画像の通り、入力すれば、appkeyが手に入ります。
composer require pusher/pusher-php-server
npm install --save-dev laravel-echo pusher-js
App\Providers\AuthServiceProvider::class,
//コメントアウトを解除するだけ。
App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
/**
* コメントアウトを解除するだけ
*/
import Echo from 'laravel-echo';
window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
forceTLS: true
});
#(上の方に外れておいてある忘れずにlogからpusherに書き換える。)
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=5565431
PUSHER_APP_KEY=395bdfe72a63c666dfaee96e
PUSHER_APP_SECRET=defaeab0f150493166ed94
PUSHER_APP_CLUSTER=ap3
npm run dev
######2.イベントを作成する
php artisan make:event SendMessage
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class SendMessage
//追加 \(^o^)/ 忘れたら アカン!!!!
implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $chat;
public function __construct($chat)
{
$this->chat = $chat;
}
public function broadcastOn()
{
return new Channel('chat.' . $this->chat->to_user_id);
}
/**
*追加
*/
public function broadcastWith()
{
return [
'message' => $this->chat->message,
'created_at' => $this->chat->created_at,
];
}
/**
* イベントブロードキャスト名
*/
public function broadcastAs()
{
return 'chat_event';
}
}
######3.イベントをブロードキャストする
UserControllerのstore_message()を編集する。
//+
use App\Events\SendMessage;
/**
* messageを保存する
* イベントのブロードキャストを追加
*/
public function store_message(Request $request, User $user)
{
$message = $request->message;
$chat = Chat::create(['message' => $message, 'from_user_id' => Auth::id(), 'to_user_id' => $user->id]);
//+
//->toOthers()を追加することで自分以外にbroadcastできます。
broadcast(new SendMessage($chat))->toOthers();
return 'success';
}
//+
//チャンネルの購入とブロードキャストするイベントを設定する
window.Echo.channel("chat.{{ Auth::id() }}") //ok
.listen('.chat_event', (data) => { //.を忘れるな
message = user_message(data.message, new Date(data.created_at).yyyymmddhis());
$('.card-body').append(message);
$('.msg_cotainer_send:last').focus();
});
フロント側を編集しましたので、リロードしてから、確認してください。
送信とほば同時に相手側にもリアルタイムに受信ができるのを確認する。
resources\views\users\room.blade.phpの全体のコード
@extends('layouts.app')
@push('css')
<link rel="stylesheet" href="{{ asset('css/room.css') }}">
@endpush
@section('content')
<div class="container-fluid h-100">
<div class="row justify-content-center h-100">
<div class="chat" style="width:80vw;min-width:300px;max-width:550px;">
<div class="card">
<div class="card-header msg_head bg-secondary">
<div class="d-flex bd-highlight">
<div class="img_cont">
<img src="{{ asset($user->img_url) }}" class="rounded-circle user_img">
</div>
<div class="user_info">
<span>{{ $user->name }}</span>
<p>message count : {{ $user->get_room_messages_count }}</p>
</div>
</div>
</div>
<div class="card-body msg_card_body">
</div>
<div class="card-footer bg-secondary">
<div class="input-group">
<div class="input-group-append">
<span class="input-group-text attach_btn"><i class="fas fa-paperclip"></i></span>
</div>
<textarea id="message" name="message" class="form-control type_msg"
placeholder="Type your message..."></textarea>
<div class="input-group-append">
<span class="input-group-text send_btn"><i class="fas fa-location-arrow"></i></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('js')
<script>
$(document).ready(function() {
$('.send_btn').on('click', function() {
message = $('#message').val()
send_message(message);
});
//post送信するためcsrfが必須。
function send_message(message) {
if (!message) {
return false;
}
create_message = auth_message(message, msg_time)
$('.card-body').append(create_message);
$('#message').val('');
$.ajax({
type: 'post',
datatype: 'json',
url: "{{ route('store_message', $user) }}",
timeout: 3000,
data: {
'message': message
}, //送信するデータを指定
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
});
}
});
//htmlを作成する
new Date().toLocaleString({
timeZone: 'Asia/Tokyo'
})
Date.prototype.yyyymmddhis = function() {
var yyyy = this.getFullYear().toString();
var mm = (this.getMonth() + 1).toString(); // getMonth() is zero-based
var dd = this.getDate().toString();
var h = this.getHours().toString();
var m = this.getMinutes().toString();
var s = this.getSeconds().toString();
return yyyy + "/" + (mm[1] ? mm : "0" + mm[0]) + "/" + (dd[1] ? dd : "0" + dd[0]) +
" " + (h[1] ? h : "0" + h[0]) + ":" + (m[1] ? m : "0" + m[0]) + ":" + (s[1] ? s : "0" + s[
0]); // padding
};
msg_time = new Date();
msg_time = msg_time.yyyymmddhis()
auth_img_url = "{{ Auth::user()->img_url }}"
function auth_message(message, msg_time) {
if (!message) {
return false;
}
create_message = `<div class="d-flex justify-content-end mb-4">`;
create_message += '<div class="msg_cotainer_send">';
create_message += `${message.replace(/\n/g, '<br>')}`;
create_message += `<span class="msg_time_send text-nowrap">${msg_time}</span>`;
create_message += '</div>';
create_message += '<div class="img_cont_msg">';
create_message += `<img src="${auth_img_url}" class="rounded-circle user_img_msg"></div></div>`;
return create_message;
}
//ajaxでメッセージを取得
//+
getMessages()
//+
function getMessages() {
$.ajax({
type: 'get',
datatype: 'json',
url: "{{ route('get_messages', $user) }}",
timeout: 3000,
})
.done(function(data, textStatus, jqXHR) {
auth_id = {{ Auth::id() }}
$result = $(".card-body"),
messages = [];
$.each(data, function(index, value) {
message = ``;
if (auth_id == value.from_user_id) {
message += auth_message(value.message, value.created_at);
} else {
message += user_message(value.message, value.created_at);
}
messages.push(message);
});
$result[0].innerHTML = messages.join("");
});
}
//htmlの作成
//+
user_img_url = "{{ $user->img_url }}";
//+
function user_message(message, msg_time) {
create_message = `<div class="d-flex justify-content-start mb-4">`;
create_message += '<div class="img_cont_msg">';
create_message += `<img src="${user_img_url}" class="rounded-circle user_img_msg"></div>`;
create_message += '<div class="msg_cotainer">';
create_message += `${message.replace(/\n/g, '<br>')}`;
create_message += `<span class="msg_time text-nowrap">${msg_time}</span>`;
create_message += '</div></div>';
return create_message;
}
//+
//チャンネルの購入とブロードキャストするイベントを設定する
window.Echo.channel("chat.{{ Auth::id() }}") //ok
.listen('.chat_event', (data) => { //.を忘れるな
message = user_message(data.message, new Date(data.created_at).yyyymmddhis());
$('.card-body').append(message).animate({scrollTop:$('.msg_cotainer:last').offset().top});
});
</script>
@endpush
<img src="" />
タグで送ると画像まで表示されてしまう。
更にscript
を送ると。
//チャンネルの購入とブロードキャストするイベントを設定する
window.Echo.channel("chat.{{ Auth::id() }}") //ok
.listen('.chat_event', (data) => { //.を忘れるな
//+修正する
// message = user_message(data.message, new Date(data.created_at).yyyymmddhis());
es_message = escape_html(data.message);
message = user_message(es_message, new Date(data.created_at).yyyymmddhis());
$('.card-body').append(message).animate({scrollTop:$('.msg_cotainer:last').offset().top});
});
//+
function escape_html(string) {
if (typeof string !== 'string') {
return string;
}
return string.replace(/[&'`"<>]/g, function(match) {
return {
'&': '&',
"'": ''',
'`': '`',
'"': '"',
'<': '<',
'>': '>',
} [match]
});
}
サーバー側でもエスケープする。
public function store_message(Request $request, User $user)
{
//+e();を追加
$message = e($request->message);
$chat = Chat::create(['message' => $message, 'from_user_id' => Auth::id(), 'to_user_id' => $user->id]);
broadcast(new SendMessage($chat))->toOthers();
return 'success';
}
画像も含めて完全にエスケープされるようになった。