やるだけ.tsx

This commit is contained in:
shibafu 2020-08-17 16:00:04 +09:00
parent e961a2d4b4
commit 6968ca7333
12 changed files with 332 additions and 556 deletions

View File

@ -16,17 +16,26 @@ class EjaculationController extends Controller
{
public function create(Request $request)
{
$defaults = [
'date' => $request->input('date', date('Y/m/d')),
'time' => $request->input('time', date('H:i')),
'link' => $request->input('link', ''),
'tags' => $request->input('tags', ''),
'note' => $request->input('note', ''),
'is_private' => $request->input('is_private', 0) == 1,
'is_too_sensitive' => $request->input('is_too_sensitive', 0) == 1
$tags = old('tags') ?? $request->input('tags', '');
if (!empty($tags)) {
$tags = explode(' ', $tags);
}
$errors = $request->session()->get('errors');
$initialState = [
'fields' => [
'date' => old('date') ?? $request->input('date', date('Y/m/d')),
'time' => old('time') ?? $request->input('time', date('H:i')),
'link' => old('link') ?? $request->input('link', ''),
'tags' => $tags,
'note' => old('note') ?? $request->input('note', ''),
'is_private' => old('is_private') ?? $request->input('is_private', 0) == 1,
'is_too_sensitive' => old('is_too_sensitive') ?? $request->input('is_too_sensitive', 0) == 1
],
'errors' => isset($errors) ? $errors->getMessages() : null
];
return view('ejaculation.checkin')->with('defaults', $defaults);
return view('ejaculation.checkin')->with('initialState', $initialState);
}
public function store(Request $request)
@ -112,13 +121,36 @@ class EjaculationController extends Controller
return view('ejaculation.show')->with(compact('user', 'ejaculation', 'ejaculatedSpan'));
}
public function edit($id)
public function edit(Request $request, $id)
{
$ejaculation = Ejaculation::findOrFail($id);
$this->authorize('edit', $ejaculation);
return view('ejaculation.edit')->with(compact('ejaculation'));
if (old('tags') === null) {
$tags = $ejaculation->tags->pluck('name');
} else {
$tags = old('tags');
if (!empty($tags)) {
$tags = explode(' ', $tags);
}
}
$errors = $request->session()->get('errors');
$initialState = [
'fields' => [
'date' => old('date') ?? $ejaculation->ejaculated_date->format('Y/m/d'),
'time' => old('time') ?? $ejaculation->ejaculated_date->format('H:i'),
'link' => old('link') ?? $ejaculation->link,
'tags' => $tags,
'note' => old('note') ?? $ejaculation->note,
'is_private' => is_bool(old('is_private')) ? old('is_private') : $ejaculation->note,
'is_too_sensitive' => is_bool(old('is_too_sensitive')) ? old('is_too_sensitive') : $ejaculation->is_too_sensitive
],
'errors' => isset($errors) ? $errors->getMessages() : null
];
return view('ejaculation.edit')->with(compact('ejaculation', 'initialState'));
}
public function update(Request $request, $id)

View File

@ -1,87 +1,6 @@
import Vue from 'vue';
import TagInput from './components/TagInput.vue';
import MetadataPreview from './components/MetadataPreview.vue';
import { fetchGet, ResponseError } from './fetch';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { TagInput as TagInput2 } from './components/TagInput2';
import { CheckinForm } from './components/CheckinForm';
export const bus = new Vue({ name: 'EventBus' });
export enum MetadataLoadState {
Inactive,
Loading,
Success,
Failed,
}
export type Metadata = {
url: string;
title: string;
description: string;
image: string;
expires_at: string | null;
tags: {
name: string;
}[];
};
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;
});
},
},
});
ReactDOM.render(
<TagInput2 id={'tagInput2'} name={'tags2'} value={''} isInvalid={false} />,
document.querySelector('#tagInput2')
);
const initialState = JSON.parse(document.getElementById('initialState')?.textContent as string);
ReactDOM.render(<CheckinForm initialState={initialState}/>, document.getElementById('checkinForm'));

View File

@ -0,0 +1,187 @@
import * as React from 'react';
import { useState } from 'react';
import * as classNames from 'classnames';
import { TagInput } from './TagInput';
import { MetadataPreview } from './MetadataPreview';
type CheckboxProps = {
id: string;
name: string;
className?: string;
checked?: boolean;
onChange?: (newValue: boolean) => void;
};
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>
);
type FieldErrorProps = {
errors?: string[];
};
const FieldError: React.FC<FieldErrorProps> = ({ errors }) =>
(errors && errors.length > 0 && <div className="invalid-feedback">{errors[0]}</div>) || null;
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;
export 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>
</>
);
};

View File

@ -1,6 +1,25 @@
import * as React from 'react';
import { Metadata, MetadataLoadState } from '../checkin';
import { useEffect, useState } from 'react';
import * as 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;
@ -8,10 +27,9 @@ type Suggestion = {
};
type MetadataPreviewProps = {
state: MetadataLoadState;
metadata: Metadata | null;
link: string;
tags: string[];
handleAddTag: (tag: string) => void;
onClickTag: (tag: string) => void;
};
const MetadataLoading = () => (
@ -38,7 +56,35 @@ const MetadataLoadFailed = () => (
</div>
);
export const MetadataPreview: React.FC<MetadataPreviewProps> = ({ state, metadata, tags, handleAddTag }) => {
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;
}
@ -69,9 +115,11 @@ export const MetadataPreview: React.FC<MetadataPreviewProps> = ({ state, metadat
<MetadataLoading />
) : state === MetadataLoadState.Success ? (
<div className="row no-gutters">
<div v-if="hasImage" className="col-4 justify-content-center align-items-center">
<img src={metadata?.image} alt="Thumbnail" className="w-100 bg-secondary" />
</div>
{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>
@ -89,7 +137,7 @@ export const MetadataPreview: React.FC<MetadataPreviewProps> = ({ state, metadat
<li
key={tag.name}
className={tagClasses(tag)}
onClick={() => handleAddTag(tag.name)}
onClick={() => onClickTag(tag.name)}
>
<span className="oi oi-tag" /> {tag.name}
</li>

View File

@ -1,156 +0,0 @@
<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>

View File

@ -5,17 +5,17 @@ import * as classNames from 'classnames';
type TagInputProps = {
id: string;
name: string;
value: string;
values: string[];
isInvalid: boolean;
onChange?: (newValues: string[]) => void;
};
export const TagInput: React.FC<TagInputProps> = ({ id, name, value, isInvalid }) => {
const [tags, setTags] = useState(value.trim() !== '' ? value.trim().split(' ') : []);
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) => {
setTags(tags.filter((v, i) => i != index));
onChange && onChange(values.filter((v, i) => i != index));
};
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (buffer.trim() !== '') {
@ -24,7 +24,7 @@ export const TagInput: React.FC<TagInputProps> = ({ id, name, value, isInvalid }
case 'Enter':
case ' ':
if ((event as any).isComposing !== true) {
setTags(tags.concat(buffer.trim().replace(/\s+/g, '_')));
onChange && onChange(values.concat(buffer.trim().replace(/\s+/g, '_')));
setBuffer('');
}
event.preventDefault();
@ -33,7 +33,7 @@ export const TagInput: React.FC<TagInputProps> = ({ id, name, value, isInvalid }
// 実際にテキストボックスに入力されている文字を見に行く (フォールバック処理)
const nativeEvent = event.nativeEvent;
if (nativeEvent.srcElement && (nativeEvent.srcElement as HTMLInputElement).value.slice(-1) == ' ') {
setTags(tags.concat(buffer.trim().replace(/\s+/g, '_')));
onChange && onChange(values.concat(buffer.trim().replace(/\s+/g, '_')));
setBuffer('');
event.preventDefault();
}
@ -48,9 +48,9 @@ export const TagInput: React.FC<TagInputProps> = ({ id, name, value, isInvalid }
return (
<div className={containerClass} onClick={() => inputRef.current?.focus()}>
<input name={name} type="hidden" value={tags.join(' ')} />
<input name={name} type="hidden" value={values.join(' ')} />
<ul className="list-inline d-inline">
{tags.map((tag, i) => (
{values.map((tag, i) => (
<li
key={i}
className={classNames('list-inline-item', 'badge', 'badge-primary', 'tis-tag-input-item')}
@ -59,16 +59,18 @@ export const TagInput: React.FC<TagInputProps> = ({ id, name, value, isInvalid }
<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>
<input
id={id}
ref={inputRef}
type="text"
className="tis-tag-input-field"
value={buffer}
onChange={(e) => setBuffer(e.target.value)}
onKeyDown={onKeyDown}
/>
</div>
);
};

View File

@ -1,96 +0,0 @@
<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>

View File

@ -14,6 +14,7 @@ $primary: #e53fb1;
@import "components/ejaculation";
@import "components/link-card";
@import "components/tag-input";
@import "components/metadata-preview";
// Tag
@import "tag/index";

View File

@ -2,7 +2,6 @@
&-link-card {
$height: 150px;
overflow: hidden;
font-size: small;
.row > div:first-child {
display: flex;
@ -16,7 +15,12 @@
}
}
.card-title {
font-size: small;
}
.card-text {
font-size: small;
white-space: pre-line;
}
}

View File

@ -1,8 +1,11 @@
.tis-tag-input-item {
cursor: pointer;
.tis-tag-input {
&-item {
cursor: pointer;
}
&-field {
border: 0;
outline: 0;
}
}
.tis-tag-input-field {
border: 0;
outline: 0;
}

View File

@ -3,101 +3,15 @@
@section('title', 'チェックイン')
@section('content')
<div id="app" class="container">
<div 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 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>
<div id="tagInput2"></div>
<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 id="metadataPreview2"></div>
<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 id="checkinForm">
<div class="text-center small" style="height: 640px;">しばらくお待ちください…</div>
</div>
</form>
<p class="text-center small mt-4"><strong>Tips</strong>: ブックマークレットや共有機能で、簡単にチェックインできます! <a href="{{ route('checkin.tools') }}" target="_blank" rel="noopener">使い方はこちら</a></p>
@ -107,5 +21,6 @@
@endsection
@push('script')
<script id="initialState" type="application/json">@json($initialState)</script>
<script src="{{ mix('js/checkin.js') }}"></script>
@endpush

View File

@ -3,7 +3,7 @@
@section('title', 'チェックインの修正')
@section('content')
<div id="app" class="container">
<div class="container">
<h2>チェックインの修正</h2>
<hr>
<div class="row justify-content-center mt-5">
@ -11,92 +11,8 @@
<form method="post" action="{{ route('checkin.update', ['id' => $ejaculation->id]) }}">
{{ method_field('PUT') }}
{{ csrf_field() }}
<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 id="checkinForm">
<div class="text-center small" style="height: 640px;">しばらくお待ちください…</div>
</div>
</form>
</div>
@ -105,5 +21,6 @@
@endsection
@push('script')
<script id="initialState" type="application/json">@json($initialState)</script>
<script src="{{ mix('js/checkin.js') }}"></script>
@endpush