Revert "Release 20200823.1100"
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import * as Cookies from 'js-cookie';
|
||||
import { fetchPostJson, fetchDeleteJson, ResponseError } from './fetch';
|
||||
import { linkCard, pageSelector, deleteCheckinModal } from './tissue';
|
||||
|
||||
|
68
resources/assets/js/checkin.ts
Normal file
68
resources/assets/js/checkin.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import Vue from 'vue';
|
||||
import TagInput from './components/TagInput.vue';
|
||||
import MetadataPreview from './components/MetadataPreview.vue';
|
||||
import { fetchGet, ResponseError } from './fetch';
|
||||
|
||||
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,
|
||||
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;
|
||||
|
||||
fetchGet('/api/checkin/card', { url })
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
this.metadata = data;
|
||||
this.metadataLoadState = MetadataLoadState.Success;
|
||||
})
|
||||
.catch(() => {
|
||||
this.metadata = null;
|
||||
this.metadataLoadState = MetadataLoadState.Failed;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { CheckinForm } from './components/CheckinForm';
|
||||
|
||||
const initialState = JSON.parse(document.getElementById('initialState')?.textContent as string);
|
||||
ReactDOM.render(<CheckinForm initialState={initialState} />, document.getElementById('checkinForm'));
|
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
type CheckboxProps = {
|
||||
id: string;
|
||||
name: string;
|
||||
className?: string;
|
||||
checked?: boolean;
|
||||
onChange?: (newValue: boolean) => void;
|
||||
};
|
||||
|
||||
export const CheckBox: React.FC<CheckboxProps> = ({ id, name, className, checked, onChange, children }) => (
|
||||
<div className={`custom-control custom-checkbox ${className}`}>
|
||||
<input
|
||||
id={id}
|
||||
name={name}
|
||||
type="checkbox"
|
||||
className="custom-control-input"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange && onChange(e.target.checked)}
|
||||
/>
|
||||
<label className="custom-control-label" htmlFor={id}>
|
||||
{children}
|
||||
</label>
|
||||
</div>
|
||||
);
|
@@ -1,149 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { CheckBox } from './CheckBox';
|
||||
import { FieldError, StandaloneFieldError } from './FieldError';
|
||||
import { TagInput } from './TagInput';
|
||||
import { MetadataPreview } from './MetadataPreview';
|
||||
|
||||
type CheckinFormProps = {
|
||||
initialState: any;
|
||||
};
|
||||
|
||||
export const CheckinForm: React.FC<CheckinFormProps> = ({ initialState }) => {
|
||||
const [date, setDate] = useState<string>(initialState.fields.date || '');
|
||||
const [time, setTime] = useState<string>(initialState.fields.time || '');
|
||||
const [tags, setTags] = useState<string[]>(initialState.fields.tags || []);
|
||||
const [link, setLink] = useState<string>(initialState.fields.link || '');
|
||||
const [linkForPreview, setLinkForPreview] = useState(link);
|
||||
const [note, setNote] = useState<string>(initialState.fields.note || '');
|
||||
const [isPrivate, setPrivate] = useState<boolean>(!!initialState.fields.is_private);
|
||||
const [isTooSensitive, setTooSensitive] = useState<boolean>(!!initialState.fields.is_too_sensitive);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="form-row">
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor="date">
|
||||
<span className="oi oi-calendar" /> 日付
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="date"
|
||||
name="date"
|
||||
className={classNames({
|
||||
'form-control': true,
|
||||
'is-invalid': initialState.errors?.date || initialState.errors?.datetime,
|
||||
})}
|
||||
pattern="^20[0-9]{2}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$"
|
||||
required
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
/>
|
||||
<FieldError errors={initialState.errors?.date} />
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor="time">
|
||||
<span className="oi oi-clock" /> 時刻
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="time"
|
||||
name="time"
|
||||
className={classNames({
|
||||
'form-control': true,
|
||||
'is-invalid': initialState.errors?.time || initialState.errors?.datetime,
|
||||
})}
|
||||
pattern="^([01][0-9]|2[0-3]):[0-5][0-9]$"
|
||||
required
|
||||
value={time}
|
||||
onChange={(e) => setTime(e.target.value)}
|
||||
/>
|
||||
<FieldError errors={initialState.errors?.time} />
|
||||
</div>
|
||||
<StandaloneFieldError errors={initialState.errors?.datetime} />
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group col-sm-12">
|
||||
<label htmlFor="tagInput">
|
||||
<span className="oi oi-tags" /> タグ
|
||||
</label>
|
||||
<TagInput
|
||||
id="tagInput"
|
||||
name="tags"
|
||||
values={tags}
|
||||
isInvalid={!!initialState.errors?.tags}
|
||||
onChange={(v) => setTags(v)}
|
||||
/>
|
||||
<small className="form-text text-muted">Tab, Enter, 半角スペースのいずれかで入力確定します。</small>
|
||||
<FieldError errors={initialState.errors?.tags} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group col-sm-12">
|
||||
<label htmlFor="link">
|
||||
<span className="oi oi-link-intact" /> オカズリンク
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="link"
|
||||
name="link"
|
||||
autoComplete="off"
|
||||
className={classNames({ 'form-control': true, 'is-invalid': initialState.errors?.link })}
|
||||
placeholder="http://..."
|
||||
value={link}
|
||||
onChange={(e) => setLink(e.target.value)}
|
||||
onBlur={() => setLinkForPreview(link)}
|
||||
/>
|
||||
<small className="form-text text-muted">オカズのURLを貼り付けて登録することができます。</small>
|
||||
<FieldError errors={initialState.errors?.link} />
|
||||
</div>
|
||||
</div>
|
||||
<MetadataPreview link={linkForPreview} tags={tags} onClickTag={(v) => setTags(tags.concat(v))} />
|
||||
<div className="form-row">
|
||||
<div className="form-group col-sm-12">
|
||||
<label htmlFor="note">
|
||||
<span className="oi oi-comment-square" /> ノート
|
||||
</label>
|
||||
<textarea
|
||||
id="note"
|
||||
name="note"
|
||||
className={classNames({ 'form-control': true, 'is-invalid': initialState.errors?.note })}
|
||||
rows={4}
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
/>
|
||||
<small className="form-text text-muted">最大 500 文字</small>
|
||||
<FieldError errors={initialState.errors?.note} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row mt-4">
|
||||
<p>オプション</p>
|
||||
<div className="form-group col-sm-12">
|
||||
<CheckBox
|
||||
id="isPrivate"
|
||||
name="is_private"
|
||||
className="mb-3"
|
||||
checked={isPrivate}
|
||||
onChange={(v) => setPrivate(v)}
|
||||
>
|
||||
<span className="oi oi-lock-locked" /> このチェックインを非公開にする
|
||||
</CheckBox>
|
||||
<CheckBox
|
||||
id="isTooSensitive"
|
||||
name="is_too_sensitive"
|
||||
className="mb-3"
|
||||
checked={isTooSensitive}
|
||||
onChange={(v) => setTooSensitive(v)}
|
||||
>
|
||||
<span className="oi oi-warning" /> チェックイン対象のオカズをより過激なオカズとして設定する
|
||||
</CheckBox>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<button className="btn btn-primary" type="submit">
|
||||
チェックイン
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
type FieldErrorProps = {
|
||||
errors?: string[];
|
||||
};
|
||||
|
||||
export const FieldError: React.FC<FieldErrorProps> = ({ errors }) =>
|
||||
(errors && errors.length > 0 && <div className="invalid-feedback">{errors[0]}</div>) || null;
|
||||
|
||||
export const StandaloneFieldError: React.FC<FieldErrorProps> = ({ errors }) =>
|
||||
(errors && errors.length > 0 && (
|
||||
<div className="form-group col-sm-12" style={{ marginTop: '-1rem' }}>
|
||||
<small className="text-danger">{errors[0]}</small>
|
||||
</div>
|
||||
)) ||
|
||||
null;
|
@@ -1,157 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { fetchGet, ResponseError } from '../fetch';
|
||||
|
||||
enum MetadataLoadState {
|
||||
Inactive,
|
||||
Loading,
|
||||
Success,
|
||||
Failed,
|
||||
}
|
||||
|
||||
type Metadata = {
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
expires_at: string | null;
|
||||
tags: {
|
||||
name: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type Suggestion = {
|
||||
name: string;
|
||||
used: boolean;
|
||||
};
|
||||
|
||||
type MetadataPreviewProps = {
|
||||
link: string;
|
||||
tags: string[];
|
||||
onClickTag: (tag: string) => void;
|
||||
};
|
||||
|
||||
const MetadataLoading = () => (
|
||||
<div className="row no-gutters">
|
||||
<div className="col-12">
|
||||
<div className="card-body">
|
||||
<h6 className="card-title text-center font-weight-bold text-info">
|
||||
<span className="oi oi-loop-circular" /> オカズの情報を読み込んでいます…
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const MetadataLoadFailed = () => (
|
||||
<div className="row no-gutters">
|
||||
<div className="col-12">
|
||||
<div className="card-body">
|
||||
<h6 className="card-title text-center font-weight-bold text-danger">
|
||||
<span className="oi oi-circle-x" /> オカズの情報を読み込めませんでした
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const MetadataPreview: React.FC<MetadataPreviewProps> = ({ link, tags, onClickTag }) => {
|
||||
const [state, setState] = useState(MetadataLoadState.Inactive);
|
||||
const [metadata, setMetadata] = useState<Metadata | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (link.trim() === '' || !/^https?:\/\//.test(link)) {
|
||||
setState(MetadataLoadState.Inactive);
|
||||
setMetadata(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(MetadataLoadState.Loading);
|
||||
fetchGet('/api/checkin/card', { url: link })
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
setState(MetadataLoadState.Success);
|
||||
setMetadata(data);
|
||||
})
|
||||
.catch(() => {
|
||||
setState(MetadataLoadState.Failed);
|
||||
setMetadata(null);
|
||||
});
|
||||
}, [link]);
|
||||
|
||||
if (state === MetadataLoadState.Inactive) {
|
||||
return null;
|
||||
}
|
||||
const hasImage = metadata !== null && metadata.image !== '';
|
||||
const descClasses = classNames({
|
||||
'col-8': hasImage,
|
||||
'col-12': !hasImage,
|
||||
});
|
||||
const tagClasses = (s: Suggestion) =>
|
||||
classNames({
|
||||
'list-inline-item': true,
|
||||
badge: true,
|
||||
'badge-primary': !s.used,
|
||||
'badge-secondary': s.used,
|
||||
'metadata-tag-item': true,
|
||||
});
|
||||
const suggestions =
|
||||
metadata?.tags.map((t) => ({
|
||||
name: t.name,
|
||||
used: tags.indexOf(t.name) !== -1,
|
||||
})) ?? [];
|
||||
|
||||
return (
|
||||
<div className="form-row">
|
||||
<div className="form-group col-sm-12">
|
||||
<div className="card tis-metadata-preview-link-card mb-2 px-0">
|
||||
{state === MetadataLoadState.Loading ? (
|
||||
<MetadataLoading />
|
||||
) : state === MetadataLoadState.Success ? (
|
||||
<div className="row no-gutters">
|
||||
{hasImage && (
|
||||
<div className="col-4 justify-content-center align-items-center">
|
||||
<img src={metadata?.image} alt="Thumbnail" className="w-100 bg-secondary" />
|
||||
</div>
|
||||
)}
|
||||
<div className={descClasses}>
|
||||
<div className="card-body">
|
||||
<h6 className="card-title font-weight-bold">{metadata?.title}</h6>
|
||||
{suggestions.length > 0 && (
|
||||
<>
|
||||
<p className="card-text mb-2">
|
||||
タグ候補
|
||||
<br />
|
||||
<span className="text-secondary">
|
||||
(クリックするとタグ入力欄にコピーできます)
|
||||
</span>
|
||||
</p>
|
||||
<ul className="list-inline d-inline">
|
||||
{suggestions.map((tag) => (
|
||||
<li
|
||||
key={tag.name}
|
||||
className={tagClasses(tag)}
|
||||
onClick={() => onClickTag(tag.name)}
|
||||
>
|
||||
<span className="oi oi-tag" /> {tag.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<MetadataLoadFailed />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
156
resources/assets/js/components/MetadataPreview.vue
Normal file
156
resources/assets/js/components/MetadataPreview.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<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="w-100 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="tagClasses(tag)"
|
||||
@click="addTag(tag.name)"
|
||||
:key="tag.name"
|
||||
>
|
||||
<span class="oi oi-tag"></span> {{ tag.name }}
|
||||
</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;
|
||||
}[];
|
||||
};
|
||||
|
||||
type Suggestion = {
|
||||
name: string;
|
||||
used: boolean;
|
||||
};
|
||||
|
||||
@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;
|
||||
|
||||
tags: string[] = [];
|
||||
|
||||
created(): void {
|
||||
bus.$on('change-tag', (tags: string[]) => (this.tags = tags));
|
||||
bus.$emit('resend-tag');
|
||||
}
|
||||
|
||||
addTag(tag: string): void {
|
||||
bus.$emit('add-tag', tag);
|
||||
}
|
||||
|
||||
tagClasses(s: Suggestion) {
|
||||
return {
|
||||
'list-inline-item': true,
|
||||
badge: true,
|
||||
'badge-primary': !s.used,
|
||||
'badge-secondary': s.used,
|
||||
'metadata-tag-item': true,
|
||||
};
|
||||
}
|
||||
|
||||
get suggestions(): Suggestion[] {
|
||||
if (this.metadata === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.metadata.tags.map((t) => {
|
||||
return {
|
||||
name: t.name,
|
||||
used: this.tags.indexOf(t.name) !== -1,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
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>
|
@@ -1,75 +0,0 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type TagInputProps = {
|
||||
id: string;
|
||||
name: string;
|
||||
values: string[];
|
||||
isInvalid: boolean;
|
||||
onChange?: (newValues: string[]) => void;
|
||||
};
|
||||
|
||||
export const TagInput: React.FC<TagInputProps> = ({ id, name, values, isInvalid, onChange }) => {
|
||||
const [buffer, setBuffer] = useState('');
|
||||
const containerClass = classNames('form-control', 'h-auto', { 'is-invalid': isInvalid });
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const removeTag = (index: number) => {
|
||||
onChange && onChange(values.filter((v, i) => i != index));
|
||||
};
|
||||
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (buffer.trim() !== '') {
|
||||
switch (event.key) {
|
||||
case 'Tab':
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if ((event as any).isComposing !== true) {
|
||||
onChange && onChange(values.concat(buffer.trim().replace(/\s+/g, '_')));
|
||||
setBuffer('');
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'Unidentified': {
|
||||
// 実際にテキストボックスに入力されている文字を見に行く (フォールバック処理)
|
||||
const nativeEvent = event.nativeEvent;
|
||||
if (nativeEvent.srcElement && (nativeEvent.srcElement as HTMLInputElement).value.slice(-1) == ' ') {
|
||||
onChange && onChange(values.concat(buffer.trim().replace(/\s+/g, '_')));
|
||||
setBuffer('');
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (event.key === 'Enter') {
|
||||
// 誤爆防止
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={containerClass} onClick={() => inputRef.current?.focus()}>
|
||||
<input name={name} type="hidden" value={values.join(' ')} />
|
||||
<ul className="list-inline d-inline">
|
||||
{values.map((tag, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className={classNames('list-inline-item', 'badge', 'badge-primary', 'tis-tag-input-item')}
|
||||
onClick={() => removeTag(i)}
|
||||
>
|
||||
<span className="oi oi-tag" /> {tag} | x
|
||||
</li>
|
||||
))}
|
||||
<li className="list-inline-item">
|
||||
<input
|
||||
id={id}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="tis-tag-input-field"
|
||||
value={buffer}
|
||||
onChange={(e) => setBuffer(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
96
resources/assets/js/components/TagInput.vue
Normal file
96
resources/assets/js/components/TagInput.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div :class="containerClass" @click="$refs.input.focus()">
|
||||
<input :name="name" type="hidden" :value="tagValue" />
|
||||
<ul class="list-inline d-inline">
|
||||
<li
|
||||
v-for="(tag, i) in tags"
|
||||
class="list-inline-item badge badge-primary tag-item"
|
||||
@click="removeTag(i)"
|
||||
:key="tag"
|
||||
>
|
||||
<span class="oi oi-tag"></span> {{ tag }} | x
|
||||
</li>
|
||||
</ul>
|
||||
<input :id="id" ref="input" type="text" class="tag-input" v-model="buffer" @keydown="onKeyDown" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { bus } from '../checkin';
|
||||
|
||||
@Component
|
||||
export default class TagInput extends Vue {
|
||||
@Prop(String) readonly id!: string;
|
||||
@Prop(String) readonly name!: string;
|
||||
@Prop(String) readonly value!: string;
|
||||
@Prop(Boolean) readonly isInvalid!: boolean;
|
||||
|
||||
tags: string[] = this.value.trim() !== '' ? this.value.trim().split(' ') : [];
|
||||
buffer = '';
|
||||
|
||||
created() {
|
||||
bus.$on('add-tag', (tag: string) => this.tags.indexOf(tag) === -1 && this.tags.push(tag));
|
||||
bus.$on('resend-tag', () => bus.$emit('change-tag', this.tags));
|
||||
}
|
||||
|
||||
onKeyDown(event: KeyboardEvent) {
|
||||
if (this.buffer.trim() !== '') {
|
||||
switch (event.key) {
|
||||
case 'Tab':
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if ((event as any).isComposing !== true) {
|
||||
this.tags.push(this.buffer.trim().replace(/\s+/g, '_'));
|
||||
this.buffer = '';
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'Unidentified':
|
||||
// 実際にテキストボックスに入力されている文字を見に行く (フォールバック処理)
|
||||
if (event.srcElement && (event.srcElement as HTMLInputElement).value.slice(-1) == ' ') {
|
||||
this.tags.push(this.buffer.trim().replace(/\s+/g, '_'));
|
||||
this.buffer = '';
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (event.key === 'Enter') {
|
||||
// 誤爆防止
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
removeTag(index: number) {
|
||||
this.tags.splice(index, 1);
|
||||
}
|
||||
|
||||
@Watch('tags')
|
||||
onTagsChanged() {
|
||||
bus.$emit('change-tag', this.tags);
|
||||
}
|
||||
|
||||
get containerClass() {
|
||||
return {
|
||||
'form-control': true,
|
||||
'h-auto': true,
|
||||
'is-invalid': this.isInvalid,
|
||||
};
|
||||
}
|
||||
|
||||
get tagValue(): string {
|
||||
return this.tags.join(' ');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tag-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tag-input {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
</style>
|
@@ -1,4 +1,4 @@
|
||||
import Chart from 'chart.js';
|
||||
import * as Chart from 'chart.js';
|
||||
|
||||
const graph = document.getElementById('global-count-graph') as HTMLCanvasElement;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
@@ -1,32 +0,0 @@
|
||||
import ClipboardJS from 'clipboard';
|
||||
|
||||
$('.webhook-url').on('focus', function () {
|
||||
$(this).trigger('select');
|
||||
});
|
||||
|
||||
new ClipboardJS('.copy-to-clipboard', {
|
||||
target(elem: Element): Element {
|
||||
return elem.parentElement?.parentElement?.querySelector('.webhook-url') as Element;
|
||||
},
|
||||
}).on('success', (e) => {
|
||||
e.clearSelection();
|
||||
$(e.trigger).popover('show');
|
||||
});
|
||||
$('.copy-to-clipboard').on('shown.bs.popover', function () {
|
||||
setTimeout(() => $(this).popover('hide'), 3000);
|
||||
});
|
||||
|
||||
const deleteModal = document.getElementById('deleteIncomingWebhookModal');
|
||||
if (deleteModal) {
|
||||
let id: any = null;
|
||||
deleteModal.querySelector('form')?.addEventListener('submit', function () {
|
||||
this.action = this.action.replace('@', id);
|
||||
});
|
||||
document.querySelectorAll<HTMLElement>('[data-target="#deleteIncomingWebhookModal"]').forEach((el) => {
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
id = this.dataset.id;
|
||||
$(deleteModal).modal('show', this);
|
||||
});
|
||||
});
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import CalHeatMap from 'cal-heatmap';
|
||||
import * as CalHeatMap from 'cal-heatmap';
|
||||
import { subMonths } from 'date-fns';
|
||||
|
||||
if (document.getElementById('cal-heatmap')) {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import CalHeatMap from 'cal-heatmap';
|
||||
import Chart from 'chart.js';
|
||||
import * as CalHeatMap from 'cal-heatmap';
|
||||
import * as Chart from 'chart.js';
|
||||
import { addMonths, format } from 'date-fns';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
@@ -90,7 +90,7 @@ function createMonthlyGraphData(from: Date) {
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const current = addMonths(from, i);
|
||||
const yearAndMonth = format(current, 'yyyy/MM');
|
||||
const yearAndMonth = format(current, 'YYYY/MM');
|
||||
keys.push(yearAndMonth);
|
||||
values.push(graphData.monthlySum[yearAndMonth] || 0);
|
||||
}
|
||||
|
2
resources/assets/sass/app.scss
vendored
2
resources/assets/sass/app.scss
vendored
@@ -13,8 +13,6 @@ $primary: #e53fb1;
|
||||
// Components
|
||||
@import "components/ejaculation";
|
||||
@import "components/link-card";
|
||||
@import "components/tag-input";
|
||||
@import "components/metadata-preview";
|
||||
|
||||
// Tag
|
||||
@import "tag/index";
|
||||
|
@@ -1,32 +0,0 @@
|
||||
.tis-metadata-preview {
|
||||
&-link-card {
|
||||
$height: 150px;
|
||||
overflow: hidden;
|
||||
|
||||
.row > div:first-child {
|
||||
display: flex;
|
||||
|
||||
&:not([display='none']) {
|
||||
min-height: $height;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: small;
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
|
||||
&-tag-item {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
11
resources/assets/sass/components/_tag-input.scss
vendored
11
resources/assets/sass/components/_tag-input.scss
vendored
@@ -1,11 +0,0 @@
|
||||
.tis-tag-input {
|
||||
&-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-field {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
@@ -6,19 +6,14 @@
|
||||
</h5>
|
||||
</div>
|
||||
<!-- tags -->
|
||||
@if ($ejaculation->is_private || $ejaculation->source !== 'web' || $ejaculation->tags->isNotEmpty())
|
||||
@if ($ejaculation->is_private || $ejaculation->source === 'csv' || $ejaculation->tags->isNotEmpty())
|
||||
<p class="mb-2">
|
||||
@if ($ejaculation->is_private)
|
||||
<span class="badge badge-warning"><span class="oi oi-lock-locked"></span> 非公開</span>
|
||||
@endif
|
||||
@switch ($ejaculation->source)
|
||||
@case ('csv')
|
||||
<span class="badge badge-info"><span class="oi oi-cloud-upload"></span> インポート</span>
|
||||
@break
|
||||
@case ('webhook')
|
||||
<span class="badge badge-info" data-toggle="tooltip" title="Webhookからチェックイン"><span class="oi oi-flash"></span></span>
|
||||
@break
|
||||
@endswitch
|
||||
@if ($ejaculation->source === 'csv')
|
||||
<span class="badge badge-info"><span class="oi oi-cloud-upload"></span> インポート</span>
|
||||
@endif
|
||||
@foreach ($ejaculation->tags as $tag)
|
||||
<a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
|
||||
@endforeach
|
||||
|
@@ -3,15 +3,99 @@
|
||||
@section('title', 'チェックイン')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div id="app" class="container">
|
||||
<h2>今致してる?</h2>
|
||||
<hr>
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-lg-6">
|
||||
<form method="post" action="{{ route('checkin') }}">
|
||||
{{ csrf_field() }}
|
||||
<div id="checkinForm">
|
||||
<div class="text-center small" style="height: 640px;">しばらくお待ちください…</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-sm-6">
|
||||
<label for="date"><span class="oi oi-calendar"></span> 日付</label>
|
||||
<input id="date" name="date" type="text" class="form-control {{ $errors->has('date') || $errors->has('datetime') ? ' is-invalid' : '' }}"
|
||||
pattern="^20[0-9]{2}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$" value="{{ old('date') ?? $defaults['date'] }}" required>
|
||||
|
||||
@if ($errors->has('date'))
|
||||
<div class="invalid-feedback">{{ $errors->first('date') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="form-group col-sm-6">
|
||||
<label for="time"><span class="oi oi-clock"></span> 時刻</label>
|
||||
<input id="time" name="time" type="text" class="form-control {{ $errors->has('time') || $errors->has('datetime') ? ' is-invalid' : '' }}"
|
||||
pattern="^([01][0-9]|2[0-3]):[0-5][0-9]$" value="{{ old('time') ?? $defaults['time'] }}" required>
|
||||
|
||||
@if ($errors->has('time'))
|
||||
<div class="invalid-feedback">{{ $errors->first('time') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@if ($errors->has('datetime'))
|
||||
<div class="form-group col-sm-12" style="margin-top: -1rem;">
|
||||
<small class="text-danger">{{ $errors->first('datetime') }}</small>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-sm-12">
|
||||
<label for="tagInput"><span class="oi oi-tags"></span> タグ</label>
|
||||
<tag-input id="tagInput" name="tags" value="{{ old('tags') ?? $defaults['tags'] }}" :is-invalid="{{ $errors->has('tags') ? 'true' : 'false' }}"></tag-input>
|
||||
<small class="form-text text-muted">
|
||||
Tab, Enter, 半角スペースのいずれかで入力確定します。
|
||||
</small>
|
||||
|
||||
@if ($errors->has('tags'))
|
||||
<div class="invalid-feedback">{{ $errors->first('tags') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<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'] }}"
|
||||
@change="onChangeLink">
|
||||
<small class="form-text text-muted">
|
||||
オカズのURLを貼り付けて登録することができます。
|
||||
</small>
|
||||
@if ($errors->has('link'))
|
||||
<div class="invalid-feedback">{{ $errors->first('link') }}</div>
|
||||
@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>
|
||||
<textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4">{{ old('note') ?? $defaults['note'] }}</textarea>
|
||||
<small class="form-text text-muted">
|
||||
最大 500 文字
|
||||
</small>
|
||||
@if ($errors->has('note'))
|
||||
<div class="invalid-feedback">{{ $errors->first('note') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row mt-4">
|
||||
<p>オプション</p>
|
||||
<div class="form-group col-sm-12">
|
||||
<div class="custom-control custom-checkbox mb-3">
|
||||
<input id="isPrivate" name="is_private" type="checkbox" class="custom-control-input" {{ old('is_private') || $defaults['is_private'] ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="isPrivate">
|
||||
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
|
||||
</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox mb-3">
|
||||
<input id="isTooSensitive" name="is_too_sensitive" type="checkbox" class="custom-control-input" {{ old('is_too_sensitive') || $defaults['is_too_sensitive'] ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="isTooSensitive">
|
||||
<span class="oi oi-warning"></span> チェックイン対象のオカズをより過激なオカズとして設定する
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button class="btn btn-primary" type="submit">チェックイン</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="text-center small mt-4"><strong>Tips</strong>: ブックマークレットや共有機能で、簡単にチェックインできます! <a href="{{ route('checkin.tools') }}" target="_blank" rel="noopener">使い方はこちら</a></p>
|
||||
@@ -21,6 +105,5 @@
|
||||
@endsection
|
||||
|
||||
@push('script')
|
||||
<script id="initialState" type="application/json">@json($initialState)</script>
|
||||
<script src="{{ mix('js/checkin.js') }}"></script>
|
||||
@endpush
|
||||
|
@@ -3,7 +3,7 @@
|
||||
@section('title', 'チェックインの修正')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div id="app" class="container">
|
||||
<h2>チェックインの修正</h2>
|
||||
<hr>
|
||||
<div class="row justify-content-center mt-5">
|
||||
@@ -11,8 +11,92 @@
|
||||
<form method="post" action="{{ route('checkin.update', ['id' => $ejaculation->id]) }}">
|
||||
{{ method_field('PUT') }}
|
||||
{{ csrf_field() }}
|
||||
<div id="checkinForm">
|
||||
<div class="text-center small" style="height: 640px;">しばらくお待ちください…</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-sm-6">
|
||||
<label for="date"><span class="oi oi-calendar"></span> 日付</label>
|
||||
<input id="date" name="date" type="text" class="form-control {{ $errors->has('date') || $errors->has('datetime') ? ' is-invalid' : '' }}"
|
||||
pattern="^20[0-9]{2}/(0[1-9]|1[0-2])/(0[1-9]|[12][0-9]|3[01])$" value="{{ old('date') ?? $ejaculation->ejaculated_date->format('Y/m/d') }}" required>
|
||||
|
||||
@if ($errors->has('date'))
|
||||
<div class="invalid-feedback">{{ $errors->first('date') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="form-group col-sm-6">
|
||||
<label for="time"><span class="oi oi-clock"></span> 時刻</label>
|
||||
<input id="time" name="time" type="text" class="form-control {{ $errors->has('time') || $errors->has('datetime') ? ' is-invalid' : '' }}"
|
||||
pattern="^([01][0-9]|2[0-3]):[0-5][0-9]$" value="{{ old('time') ?? $ejaculation->ejaculated_date->format('H:i') }}" required>
|
||||
|
||||
@if ($errors->has('time'))
|
||||
<div class="invalid-feedback">{{ $errors->first('time') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@if ($errors->has('datetime'))
|
||||
<div class="form-group col-sm-12" style="margin-top: -1rem;">
|
||||
<small class="text-danger">{{ $errors->first('datetime') }}</small>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-sm-12">
|
||||
<label for="tagInput"><span class="oi oi-tags"></span> タグ</label>
|
||||
<tag-input id="tagInput" name="tags" value="{{ old('tags') ?? $ejaculation->textTags() }}" :is-invalid="{{ $errors->has('tags') ? 'true' : 'false' }}"></tag-input>
|
||||
<small class="form-text text-muted">
|
||||
Tab, Enter, 半角スペースのいずれかで入力確定します。
|
||||
</small>
|
||||
|
||||
@if ($errors->has('tags'))
|
||||
<div class="invalid-feedback">{{ $errors->first('tags') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<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 }}"
|
||||
@change="onChangeLink">
|
||||
<small class="form-text text-muted">
|
||||
オカズのURLを貼り付けて登録することができます。
|
||||
</small>
|
||||
@if ($errors->has('link'))
|
||||
<div class="invalid-feedback">{{ $errors->first('link') }}</div>
|
||||
@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>
|
||||
<textarea id="note" name="note" class="form-control {{ $errors->has('note') ? ' is-invalid' : '' }}" rows="4">{{ old('note') ?? $ejaculation->note }}</textarea>
|
||||
<small class="form-text text-muted">
|
||||
最大 500 文字
|
||||
</small>
|
||||
@if ($errors->has('note'))
|
||||
<div class="invalid-feedback">{{ $errors->first('note') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row mt-4">
|
||||
<p>オプション</p>
|
||||
<div class="form-group col-sm-12">
|
||||
<div class="custom-control custom-checkbox mb-3">
|
||||
<input id="isPrivate" name="is_private" type="checkbox" class="custom-control-input" {{ (is_bool(old('is_private')) ? old('is_private') : $ejaculation->is_private) ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="isPrivate">
|
||||
<span class="oi oi-lock-locked"></span> このチェックインを非公開にする
|
||||
</label>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox mb-3">
|
||||
<input id="isTooSensitive" name="is_too_sensitive" type="checkbox" class="custom-control-input" {{ (is_bool(old('is_too_sensitive')) ? old('is_too_sensitive') : $ejaculation->is_too_sensitive) ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="isTooSensitive">
|
||||
<span class="oi oi-warning"></span> チェックイン対象のオカズをより過激なオカズとして設定する
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button class="btn btn-primary" type="submit">チェックイン</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -21,6 +105,5 @@
|
||||
@endsection
|
||||
|
||||
@push('script')
|
||||
<script id="initialState" type="application/json">@json($initialState)</script>
|
||||
<script src="{{ mix('js/checkin.js') }}"></script>
|
||||
@endpush
|
||||
|
@@ -34,19 +34,14 @@
|
||||
<h5>{{ $ejaculatedSpan ?? '精通' }} <small class="text-muted">{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ~ ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></h5>
|
||||
</div>
|
||||
<!-- tags -->
|
||||
@if ($ejaculation->is_private || $ejaculation->source !== 'web' || $ejaculation->tags->isNotEmpty())
|
||||
@if ($ejaculation->is_private || $ejaculation->source === 'csv' || $ejaculation->tags->isNotEmpty())
|
||||
<p class="mb-2">
|
||||
@if ($ejaculation->is_private)
|
||||
<span class="badge badge-warning"><span class="oi oi-lock-locked"></span> 非公開</span>
|
||||
@endif
|
||||
@switch ($ejaculation->source)
|
||||
@case ('csv')
|
||||
<span class="badge badge-info"><span class="oi oi-cloud-upload"></span> インポート</span>
|
||||
@break
|
||||
@case ('webhook')
|
||||
<span class="badge badge-info" data-toggle="tooltip" title="Webhookからチェックイン"><span class="oi oi-flash"></span></span>
|
||||
@break
|
||||
@endswitch
|
||||
@if ($ejaculation->source === 'csv')
|
||||
<span class="badge badge-info"><span class="oi oi-cloud-upload"></span> インポート</span>
|
||||
@endif
|
||||
@foreach ($ejaculation->tags as $tag)
|
||||
<a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
|
||||
@endforeach
|
||||
|
@@ -10,8 +10,6 @@
|
||||
href="{{ route('setting') }}"><span class="oi oi-person mr-1"></span> プロフィール</a>
|
||||
<a class="list-group-item list-group-item-action {{ Route::currentRouteName() === 'setting.privacy' ? 'active' : '' }}"
|
||||
href="{{ route('setting.privacy') }}"><span class="oi oi-shield mr-1"></span> プライバシー</a>
|
||||
<a class="list-group-item list-group-item-action {{ Route::currentRouteName() === 'setting.webhooks' ? 'active' : '' }}"
|
||||
href="{{ route('setting.webhooks') }}"><span class="oi oi-link-intact mr-1"></span> Webhook</a>
|
||||
<a class="list-group-item list-group-item-action {{ Route::currentRouteName() === 'setting.import' ? 'active' : '' }}"
|
||||
href="{{ route('setting.import') }}"><span class="oi oi-data-transfer-upload mr-1"></span> データのインポート</a>
|
||||
<a class="list-group-item list-group-item-action {{ Route::currentRouteName() === 'setting.export' ? 'active' : '' }}"
|
||||
|
@@ -1,70 +0,0 @@
|
||||
@extends('setting.base')
|
||||
|
||||
@section('title', 'Webhook')
|
||||
|
||||
@section('tab-content')
|
||||
<h3>Incoming Webhook</h3>
|
||||
<hr>
|
||||
<p>さまざまなシステムと連携してチェックインを行うためのWebhook URLを作成することができます。APIドキュメントは<a href="{{ url('/apidoc.html') }}">こちら</a>から参照いただけます。</p>
|
||||
<h4>新規作成</h4>
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<h6 class="font-weight-bold">おことわり</h6>
|
||||
<p>Webhook APIは予告なく仕様変更を行う場合がございます。また、サーバに対する過剰なリクエストや、不審な公開チェックインを繰り返している場合には管理者の裁量によって予告なく無効化(削除)する場合があります。</p>
|
||||
<p>通常利用と同様、1分以内のチェックインは禁止されていることを考慮してください。また、テスト目的であれば非公開チェックインをご活用ください。</p>
|
||||
<hr>
|
||||
@if (count($webhooks) >= $webhooksLimit)
|
||||
<p class="my-0 text-danger">1ユーザーが作成可能なWebhookは、{{ $webhooksLimit }}件までに制限されています。</p>
|
||||
@else
|
||||
<form action="{{ route('setting.webhooks.store') }}" method="post">
|
||||
{{ csrf_field() }}
|
||||
<div class="form-group">
|
||||
<label for="name">名前 (メモ)</label>
|
||||
<input id="name" class="form-control {{ $errors->has('name') ? ' is-invalid' : '' }}" name="name" type="text" required>
|
||||
<small class="form-text text-muted">後で分かるように名前を付けておいてください。</small>
|
||||
@if ($errors->has('name'))
|
||||
<div class="invalid-feedback">{{ $errors->first('name') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">新規作成</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if (!$webhooks->isEmpty())
|
||||
<h4 class="mt-4">作成済みのWebhook</h4>
|
||||
<div class="list-group mt-3">
|
||||
@foreach ($webhooks as $webhook)
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div class="flex-grow-1 mr-2">
|
||||
<div>{{ $webhook->name }}</div>
|
||||
<input class="webhook-url form-control form-control-sm mt-1" type="text" value="{{ url('/api/webhooks/checkin/' . $webhook->id) }}" readonly>
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<button class="btn btn-outline-secondary copy-to-clipboard" type="button" data-toggle="popover" data-trigger="manual" data-placement="top" data-content="コピーしました!">コピー</button>
|
||||
<button class="btn btn-outline-danger" type="button" data-target="#deleteIncomingWebhookModal" data-id="{{ $webhook->id }}">削除</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@component('components.modal', ['id' => 'deleteIncomingWebhookModal'])
|
||||
@slot('title')
|
||||
削除確認
|
||||
@endslot
|
||||
Webhookを削除してもよろしいですか?
|
||||
@slot('footer')
|
||||
<form action="{{ route('setting.webhooks.destroy', ['webhook' => '@']) }}" method="post">
|
||||
{{ csrf_field() }}
|
||||
{{ method_field('DELETE') }}
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">キャンセル</button>
|
||||
<button type="submit" class="btn btn-danger">削除</button>
|
||||
</form>
|
||||
@endslot
|
||||
@endcomponent
|
||||
@endsection
|
||||
|
||||
@push('script')
|
||||
<script src="{{ mix('js/setting/webhooks.js') }}"></script>
|
||||
@endpush
|
@@ -51,19 +51,14 @@
|
||||
<h5>{{ $ejaculation->ejaculated_span ?? '精通' }} <a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ~ ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a></h5>
|
||||
</div>
|
||||
<!-- tags -->
|
||||
@if ($ejaculation->is_private || $ejaculation->source !== 'web' || $ejaculation->tags->isNotEmpty())
|
||||
@if ($ejaculation->is_private || $ejaculation->source === 'csv' || $ejaculation->tags->isNotEmpty())
|
||||
<p class="mb-2">
|
||||
@if ($ejaculation->is_private)
|
||||
<span class="badge badge-warning"><span class="oi oi-lock-locked"></span> 非公開</span>
|
||||
@endif
|
||||
@switch ($ejaculation->source)
|
||||
@case ('csv')
|
||||
<span class="badge badge-info"><span class="oi oi-cloud-upload"></span> インポート</span>
|
||||
@break
|
||||
@case ('webhook')
|
||||
<span class="badge badge-info" data-toggle="tooltip" title="Webhookからチェックイン"><span class="oi oi-flash"></span></span>
|
||||
@break
|
||||
@endswitch
|
||||
@if ($ejaculation->source === 'csv')
|
||||
<span class="badge badge-info"><span class="oi oi-cloud-upload"></span> インポート</span>
|
||||
@endif
|
||||
@foreach ($ejaculation->tags as $tag)
|
||||
<a class="badge badge-secondary" href="{{ route('search', ['q' => $tag->name]) }}"><span class="oi oi-tag"></span> {{ $tag->name }}</a>
|
||||
@endforeach
|
||||
|
Reference in New Issue
Block a user