Merge pull request #224 from shikorism/feature/105-tag-suggest
チェックイン時にメタデータからタグをサジェストする
This commit is contained in:
commit
fa171bc3d3
@ -51,6 +51,8 @@ class CardController
|
|||||||
$metadata->tags()->sync($tagIds);
|
$metadata->tags()->sync($tagIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$metadata->load('tags');
|
||||||
|
|
||||||
$response = response($metadata);
|
$response = response($metadata);
|
||||||
if (!config('app.debug')) {
|
if (!config('app.debug')) {
|
||||||
$response = $response->setCache(['public' => true, 'max_age' => 86400]);
|
$response = $response->setCache(['public' => true, 'max_age' => 86400]);
|
||||||
|
@ -11,7 +11,7 @@ class Metadata extends Model
|
|||||||
protected $keyType = 'string';
|
protected $keyType = 'string';
|
||||||
|
|
||||||
protected $fillable = ['url', 'title', 'description', 'image', 'expires_at'];
|
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'];
|
protected $dates = ['created_at', 'updated_at', 'expires_at'];
|
||||||
|
|
||||||
|
@ -11,6 +11,9 @@ class Tag extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name'
|
'name'
|
||||||
];
|
];
|
||||||
|
protected $visible = [
|
||||||
|
'name'
|
||||||
|
];
|
||||||
|
|
||||||
public function ejaculations()
|
public function ejaculations()
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,66 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import TagInput from "./components/TagInput.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({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
metadata: null,
|
||||||
|
metadataLoadState: MetadataLoadState.Inactive,
|
||||||
|
},
|
||||||
components: {
|
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">
|
<script lang="ts">
|
||||||
import {Vue, Component, Prop} from "vue-property-decorator";
|
import {Vue, Component, Prop} from "vue-property-decorator";
|
||||||
|
import {bus} from "../checkin";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class TagInput extends Vue {
|
export default class TagInput extends Vue {
|
||||||
@ -28,6 +29,10 @@
|
|||||||
tags: string[] = this.value.trim() !== "" ? this.value.trim().split(" ") : [];
|
tags: string[] = this.value.trim() !== "" ? this.value.trim().split(" ") : [];
|
||||||
buffer: string = "";
|
buffer: string = "";
|
||||||
|
|
||||||
|
created() {
|
||||||
|
bus.$on("add-tag", (tag: string) => this.tags.indexOf(tag) === -1 && this.tags.push(tag));
|
||||||
|
}
|
||||||
|
|
||||||
onKeyDown(event: KeyboardEvent) {
|
onKeyDown(event: KeyboardEvent) {
|
||||||
if (this.buffer.trim() !== "") {
|
if (this.buffer.trim() !== "") {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
|
@ -52,7 +52,9 @@
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
|
<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">
|
<small class="form-text text-muted">
|
||||||
オカズのURLを貼り付けて登録することができます。
|
オカズのURLを貼り付けて登録することができます。
|
||||||
</small>
|
</small>
|
||||||
@ -61,6 +63,7 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<metadata-preview :metadata="metadata" :state="metadataLoadState"></metadata-preview>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<label for="note"><span class="oi oi-comment-square"></span> ノート</label>
|
<label for="note"><span class="oi oi-comment-square"></span> ノート</label>
|
||||||
|
@ -53,7 +53,9 @@
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<label for="link"><span class="oi oi-link-intact"></span> オカズリンク</label>
|
<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">
|
<small class="form-text text-muted">
|
||||||
オカズのURLを貼り付けて登録することができます。
|
オカズのURLを貼り付けて登録することができます。
|
||||||
</small>
|
</small>
|
||||||
@ -62,6 +64,7 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<metadata-preview :metadata="metadata" :state="metadataLoadState"></metadata-preview>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-sm-12">
|
<div class="form-group col-sm-12">
|
||||||
<label for="note"><span class="oi oi-comment-square"></span> ノート</label>
|
<label for="note"><span class="oi oi-comment-square"></span> ノート</label>
|
||||||
|
Loading…
Reference in New Issue
Block a user