Merge pull request #224 from shikorism/feature/105-tag-suggest
チェックイン時にメタデータからタグをサジェストする
This commit is contained in:
		@@ -51,6 +51,8 @@ class CardController
 | 
			
		||||
            $metadata->tags()->sync($tagIds);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $metadata->load('tags');
 | 
			
		||||
 | 
			
		||||
        $response = response($metadata);
 | 
			
		||||
        if (!config('app.debug')) {
 | 
			
		||||
            $response = $response->setCache(['public' => true, 'max_age' => 86400]);
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class Metadata extends Model
 | 
			
		||||
    protected $keyType = 'string';
 | 
			
		||||
 | 
			
		||||
    protected $fillable = ['url', 'title', 'description', 'image', 'expires_at'];
 | 
			
		||||
    protected $visible = ['url', 'title', 'description', 'image', 'expires_at'];
 | 
			
		||||
    protected $visible = ['url', 'title', 'description', 'image', 'expires_at', 'tags'];
 | 
			
		||||
 | 
			
		||||
    protected $dates = ['created_at', 'updated_at', 'expires_at'];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,9 @@ class Tag extends Model
 | 
			
		||||
    protected $fillable = [
 | 
			
		||||
        'name'
 | 
			
		||||
    ];
 | 
			
		||||
    protected $visible = [
 | 
			
		||||
        'name'
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    public function ejaculations()
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,66 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import TagInput from "./components/TagInput.vue";
 | 
			
		||||
import MetadataPreview from './components/MetadataPreview.vue';
 | 
			
		||||
 | 
			
		||||
export const bus = new Vue({name: "EventBus"});
 | 
			
		||||
 | 
			
		||||
export enum MetadataLoadState {
 | 
			
		||||
    Inactive,
 | 
			
		||||
    Loading,
 | 
			
		||||
    Success,
 | 
			
		||||
    Failed,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
new Vue({
 | 
			
		||||
    el: '#app',
 | 
			
		||||
    data: {
 | 
			
		||||
        metadata: null,
 | 
			
		||||
        metadataLoadState: MetadataLoadState.Inactive,
 | 
			
		||||
    },
 | 
			
		||||
    components: {
 | 
			
		||||
        TagInput
 | 
			
		||||
        TagInput,
 | 
			
		||||
        MetadataPreview
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        // オカズリンクにURLがセットされている場合は、すぐにメタデータを取得する
 | 
			
		||||
        const linkInput = this.$el.querySelector<HTMLInputElement>("#link");
 | 
			
		||||
        if (linkInput && /^https?:\/\//.test(linkInput.value)) {
 | 
			
		||||
            this.fetchMetadata(linkInput.value);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        // オカズリンクの変更時
 | 
			
		||||
        onChangeLink(event: Event) {
 | 
			
		||||
            if (event.target instanceof HTMLInputElement) {
 | 
			
		||||
                const url = event.target.value;
 | 
			
		||||
 | 
			
		||||
                if (url.trim() === '' || !/^https?:\/\//.test(url)) {
 | 
			
		||||
                    this.metadata = null;
 | 
			
		||||
                    this.metadataLoadState = MetadataLoadState.Inactive;
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.fetchMetadata(url);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        // メタデータの取得
 | 
			
		||||
        fetchMetadata(url: string) {
 | 
			
		||||
            this.metadataLoadState = MetadataLoadState.Loading;
 | 
			
		||||
 | 
			
		||||
            $.ajax({
 | 
			
		||||
                url: '/api/checkin/card',
 | 
			
		||||
                method: 'get',
 | 
			
		||||
                type: 'json',
 | 
			
		||||
                data: {
 | 
			
		||||
                    url
 | 
			
		||||
                }
 | 
			
		||||
            }).then(data => {
 | 
			
		||||
                this.metadata = data;
 | 
			
		||||
                this.metadataLoadState = MetadataLoadState.Success;
 | 
			
		||||
            }).catch(e => {
 | 
			
		||||
                this.metadata = null;
 | 
			
		||||
                this.metadataLoadState = MetadataLoadState.Failed;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										119
									
								
								resources/assets/js/components/MetadataPreview.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								resources/assets/js/components/MetadataPreview.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="form-row" v-if="state !== MetadataLoadState.Inactive">
 | 
			
		||||
        <div class="form-group col-sm-12">
 | 
			
		||||
            <div class="card link-card-mini mb-2 px-0">
 | 
			
		||||
                <div v-if="state === MetadataLoadState.Loading" class="row no-gutters">
 | 
			
		||||
                    <div class="col-12">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h6 class="card-title text-center font-weight-bold text-info" style="font-size: small;"><span class="oi oi-loop-circular"></span> オカズの情報を読み込んでいます…</h6>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div v-else-if="state === MetadataLoadState.Success" class="row no-gutters">
 | 
			
		||||
                    <div v-if="hasImage" class="col-4 justify-content-center align-items-center">
 | 
			
		||||
                        <img :src="metadata.image" alt="Thumbnail" class="card-img-top-to-left bg-secondary">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div :class="descClasses">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h6 class="card-title font-weight-bold" style="font-size: small;">{{ metadata.title }}</h6>
 | 
			
		||||
                            <template v-if="suggestions.length > 0">
 | 
			
		||||
                                <p class="card-text mb-2" style="font-size: small;">タグ候補<br><span class="text-secondary">(クリックするとタグ入力欄にコピーできます)</span></p>
 | 
			
		||||
                                <ul class="list-inline d-inline">
 | 
			
		||||
                                    <li v-for="tag in suggestions"
 | 
			
		||||
                                        class="list-inline-item badge badge-primary metadata-tag-item"
 | 
			
		||||
                                        @click="addTag(tag)"><span class="oi oi-tag"></span> {{ tag }}</li>
 | 
			
		||||
                                </ul>
 | 
			
		||||
                            </template>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div v-else class="row no-gutters">
 | 
			
		||||
                    <div class="col-12">
 | 
			
		||||
                        <div class="card-body">
 | 
			
		||||
                            <h6 class="card-title text-center font-weight-bold text-danger" style="font-size: small;"><span class="oi oi-circle-x"></span> オカズの情報を読み込めませんでした</h6>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {Vue, Component, Prop} from "vue-property-decorator";
 | 
			
		||||
    import {bus, MetadataLoadState} from "../checkin";
 | 
			
		||||
 | 
			
		||||
    type Metadata = {
 | 
			
		||||
        url: string,
 | 
			
		||||
        title: string,
 | 
			
		||||
        description: string,
 | 
			
		||||
        image: string,
 | 
			
		||||
        expires_at: string | null,
 | 
			
		||||
        tags: {
 | 
			
		||||
            name: string
 | 
			
		||||
        }[],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    @Component
 | 
			
		||||
    export default class MetadataPreview extends Vue {
 | 
			
		||||
        @Prop() readonly state!: MetadataLoadState;
 | 
			
		||||
        @Prop() readonly metadata!: Metadata | null;
 | 
			
		||||
 | 
			
		||||
        // for use in v-if
 | 
			
		||||
        private readonly MetadataLoadState = MetadataLoadState;
 | 
			
		||||
 | 
			
		||||
        addTag(tag: string) {
 | 
			
		||||
            bus.$emit("add-tag", tag);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        get suggestions() {
 | 
			
		||||
            if (this.metadata === null) {
 | 
			
		||||
                return [];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return this.metadata.tags.map(t => t.name);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        get hasImage() {
 | 
			
		||||
            return this.metadata !== null && this.metadata.image !== ''
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        get descClasses() {
 | 
			
		||||
            return {
 | 
			
		||||
                "col-8": this.hasImage,
 | 
			
		||||
                "col-12": !this.hasImage,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
    .link-card-mini {
 | 
			
		||||
        $height: 150px;
 | 
			
		||||
 | 
			
		||||
        .row > div {
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .row > div:first-child {
 | 
			
		||||
            display: flex;
 | 
			
		||||
 | 
			
		||||
            &:not([display=none]) {
 | 
			
		||||
                min-height: $height;
 | 
			
		||||
 | 
			
		||||
                img {
 | 
			
		||||
                    position: absolute;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .card-text {
 | 
			
		||||
            white-space: pre-line;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .metadata-tag-item {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
        user-select: none;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -17,6 +17,7 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {Vue, Component, Prop} from "vue-property-decorator";
 | 
			
		||||
    import {bus} from "../checkin";
 | 
			
		||||
 | 
			
		||||
    @Component
 | 
			
		||||
    export default class TagInput extends Vue {
 | 
			
		||||
@@ -28,6 +29,10 @@
 | 
			
		||||
        tags: string[] = this.value.trim() !== "" ? this.value.trim().split(" ") : [];
 | 
			
		||||
        buffer: string = "";
 | 
			
		||||
 | 
			
		||||
        created() {
 | 
			
		||||
            bus.$on("add-tag", (tag: string) => this.tags.indexOf(tag) === -1 && this.tags.push(tag));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        onKeyDown(event: KeyboardEvent) {
 | 
			
		||||
            if (this.buffer.trim() !== "") {
 | 
			
		||||
                switch (event.key) {
 | 
			
		||||
@@ -74,4 +79,4 @@
 | 
			
		||||
        border: 0;
 | 
			
		||||
        outline: 0;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -28,4 +28,4 @@
 | 
			
		||||
  .card-text {
 | 
			
		||||
    white-space: pre-line;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,9 @@
 | 
			
		||||
                <div class="form-row">
 | 
			
		||||
                    <div class="form-group col-sm-12">
 | 
			
		||||
                        <label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
 | 
			
		||||
                        <input id="link" name="link" type="text" autocomplete="off" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') ?? $defaults['link'] }}">
 | 
			
		||||
                        <input id="link" name="link" type="text" autocomplete="off" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}"
 | 
			
		||||
                               placeholder="http://..." value="{{ old('link') ?? $defaults['link'] }}"
 | 
			
		||||
                               @change="onChangeLink">
 | 
			
		||||
                        <small class="form-text text-muted">
 | 
			
		||||
                            オカズのURLを貼り付けて登録することができます。
 | 
			
		||||
                        </small>
 | 
			
		||||
@@ -61,6 +63,7 @@
 | 
			
		||||
                        @endif
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <metadata-preview :metadata="metadata" :state="metadataLoadState"></metadata-preview>
 | 
			
		||||
                <div class="form-row">
 | 
			
		||||
                    <div class="form-group col-sm-12">
 | 
			
		||||
                        <label for="note"><span class="oi oi-comment-square"></span> ノート</label>
 | 
			
		||||
@@ -96,4 +99,4 @@
 | 
			
		||||
 | 
			
		||||
@push('script')
 | 
			
		||||
    <script src="{{ mix('js/checkin.js') }}"></script>
 | 
			
		||||
@endpush
 | 
			
		||||
@endpush
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,9 @@
 | 
			
		||||
                <div class="form-row">
 | 
			
		||||
                    <div class="form-group col-sm-12">
 | 
			
		||||
                        <label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
 | 
			
		||||
                        <input id="link" name="link" type="text" autocomplete="off" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}" placeholder="http://..." value="{{ old('link') ?? $ejaculation->link }}">
 | 
			
		||||
                        <input id="link" name="link" type="text" autocomplete="off" class="form-control {{ $errors->has('link') ? ' is-invalid' : '' }}"
 | 
			
		||||
                               placeholder="http://..." value="{{ old('link') ?? $ejaculation->link }}"
 | 
			
		||||
                               @change="onChangeLink">
 | 
			
		||||
                        <small class="form-text text-muted">
 | 
			
		||||
                            オカズのURLを貼り付けて登録することができます。
 | 
			
		||||
                        </small>
 | 
			
		||||
@@ -62,6 +64,7 @@
 | 
			
		||||
                        @endif
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <metadata-preview :metadata="metadata" :state="metadataLoadState"></metadata-preview>
 | 
			
		||||
                <div class="form-row">
 | 
			
		||||
                    <div class="form-group col-sm-12">
 | 
			
		||||
                        <label for="note"><span class="oi oi-comment-square"></span> ノート</label>
 | 
			
		||||
@@ -97,4 +100,4 @@
 | 
			
		||||
 | 
			
		||||
@push('script')
 | 
			
		||||
    <script src="{{ mix('js/checkin.js') }}"></script>
 | 
			
		||||
@endpush
 | 
			
		||||
@endpush
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user