Merge branch 'develop' into feature/300-incoming-webhook
# Conflicts: # package.json # webpack.mix.js # yarn.lock
This commit is contained in:
commit
c680cd8d8e
18
.eslintrc.js
vendored
18
.eslintrc.js
vendored
@ -6,25 +6,33 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
'plugin:vue/essential',
|
'plugin:react/recommended',
|
||||||
'plugin:prettier/recommended',
|
'plugin:prettier/recommended',
|
||||||
'plugin:@typescript-eslint/recommended',
|
'plugin:@typescript-eslint/recommended',
|
||||||
'prettier',
|
'prettier',
|
||||||
'prettier/@typescript-eslint',
|
'prettier/@typescript-eslint',
|
||||||
'prettier/vue',
|
'prettier/react',
|
||||||
],
|
],
|
||||||
parser: 'vue-eslint-parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 11,
|
ecmaVersion: 11,
|
||||||
parser: '@typescript-eslint/parser',
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
plugins: ['prettier', 'vue', '@typescript-eslint', 'jquery'],
|
plugins: ['prettier', 'react', '@typescript-eslint', 'jquery'],
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 0,
|
'@typescript-eslint/explicit-module-boundary-types': 0,
|
||||||
'@typescript-eslint/no-explicit-any': 0,
|
'@typescript-eslint/no-explicit-any': 0,
|
||||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||||
'jquery/no-ajax': 2,
|
'jquery/no-ajax': 2,
|
||||||
'jquery/no-ajax-events': 2,
|
'jquery/no-ajax-events': 2,
|
||||||
|
'react/prop-types': 0,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -9,23 +9,33 @@ use App\User;
|
|||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Validator;
|
use Validator;
|
||||||
|
|
||||||
class EjaculationController extends Controller
|
class EjaculationController extends Controller
|
||||||
{
|
{
|
||||||
public function create(Request $request)
|
public function create(Request $request)
|
||||||
{
|
{
|
||||||
$defaults = [
|
$tags = old('tags') ?? $request->input('tags', '');
|
||||||
'date' => $request->input('date', date('Y/m/d')),
|
if (!empty($tags)) {
|
||||||
'time' => $request->input('time', date('H:i')),
|
$tags = explode(' ', $tags);
|
||||||
'link' => $request->input('link', ''),
|
}
|
||||||
'tags' => $request->input('tags', ''),
|
|
||||||
'note' => $request->input('note', ''),
|
$errors = $request->session()->get('errors');
|
||||||
'is_private' => $request->input('is_private', 0) == 1,
|
$initialState = [
|
||||||
'is_too_sensitive' => $request->input('is_too_sensitive', 0) == 1
|
'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)
|
public function store(Request $request)
|
||||||
@ -52,29 +62,33 @@ class EjaculationController extends Controller
|
|||||||
return redirect()->route('checkin')->withErrors($validator)->withInput();
|
return redirect()->route('checkin')->withErrors($validator)->withInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
$ejaculation = Ejaculation::create([
|
$ejaculation = DB::transaction(function () use ($request, $inputs) {
|
||||||
'user_id' => Auth::id(),
|
$ejaculation = Ejaculation::create([
|
||||||
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
|
'user_id' => Auth::id(),
|
||||||
'note' => $inputs['note'] ?? '',
|
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
|
||||||
'link' => $inputs['link'] ?? '',
|
'note' => $inputs['note'] ?? '',
|
||||||
'source' => Ejaculation::SOURCE_WEB,
|
'link' => $inputs['link'] ?? '',
|
||||||
'is_private' => $request->has('is_private') ?? false,
|
'source' => Ejaculation::SOURCE_WEB,
|
||||||
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false
|
'is_private' => $request->has('is_private') ?? false,
|
||||||
]);
|
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false
|
||||||
|
]);
|
||||||
|
|
||||||
$tagIds = [];
|
$tagIds = [];
|
||||||
if (!empty($inputs['tags'])) {
|
if (!empty($inputs['tags'])) {
|
||||||
$tags = explode(' ', $inputs['tags']);
|
$tags = explode(' ', $inputs['tags']);
|
||||||
foreach ($tags as $tag) {
|
foreach ($tags as $tag) {
|
||||||
if ($tag === '') {
|
if ($tag === '') {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tag = Tag::firstOrCreate(['name' => $tag]);
|
||||||
|
$tagIds[] = $tag->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tag = Tag::firstOrCreate(['name' => $tag]);
|
|
||||||
$tagIds[] = $tag->id;
|
|
||||||
}
|
}
|
||||||
}
|
$ejaculation->tags()->sync($tagIds);
|
||||||
$ejaculation->tags()->sync($tagIds);
|
|
||||||
|
return $ejaculation;
|
||||||
|
});
|
||||||
|
|
||||||
if (!empty($ejaculation->link)) {
|
if (!empty($ejaculation->link)) {
|
||||||
event(new LinkDiscovered($ejaculation->link));
|
event(new LinkDiscovered($ejaculation->link));
|
||||||
@ -107,13 +121,36 @@ class EjaculationController extends Controller
|
|||||||
return view('ejaculation.show')->with(compact('user', 'ejaculation', 'ejaculatedSpan'));
|
return view('ejaculation.show')->with(compact('user', 'ejaculation', 'ejaculatedSpan'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function edit($id)
|
public function edit(Request $request, $id)
|
||||||
{
|
{
|
||||||
$ejaculation = Ejaculation::findOrFail($id);
|
$ejaculation = Ejaculation::findOrFail($id);
|
||||||
|
|
||||||
$this->authorize('edit', $ejaculation);
|
$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)
|
public function update(Request $request, $id)
|
||||||
@ -144,27 +181,29 @@ class EjaculationController extends Controller
|
|||||||
return redirect()->route('checkin.edit', ['id' => $id])->withErrors($validator)->withInput();
|
return redirect()->route('checkin.edit', ['id' => $id])->withErrors($validator)->withInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
$ejaculation->fill([
|
DB::transaction(function () use ($ejaculation, $request, $inputs) {
|
||||||
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
|
$ejaculation->fill([
|
||||||
'note' => $inputs['note'] ?? '',
|
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
|
||||||
'link' => $inputs['link'] ?? '',
|
'note' => $inputs['note'] ?? '',
|
||||||
'is_private' => $request->has('is_private') ?? false,
|
'link' => $inputs['link'] ?? '',
|
||||||
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false
|
'is_private' => $request->has('is_private') ?? false,
|
||||||
])->save();
|
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false
|
||||||
|
])->save();
|
||||||
|
|
||||||
$tagIds = [];
|
$tagIds = [];
|
||||||
if (!empty($inputs['tags'])) {
|
if (!empty($inputs['tags'])) {
|
||||||
$tags = explode(' ', $inputs['tags']);
|
$tags = explode(' ', $inputs['tags']);
|
||||||
foreach ($tags as $tag) {
|
foreach ($tags as $tag) {
|
||||||
if ($tag === '') {
|
if ($tag === '') {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tag = Tag::firstOrCreate(['name' => $tag]);
|
||||||
|
$tagIds[] = $tag->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tag = Tag::firstOrCreate(['name' => $tag]);
|
|
||||||
$tagIds[] = $tag->id;
|
|
||||||
}
|
}
|
||||||
}
|
$ejaculation->tags()->sync($tagIds);
|
||||||
$ejaculation->tags()->sync($tagIds);
|
});
|
||||||
|
|
||||||
if (!empty($ejaculation->link)) {
|
if (!empty($ejaculation->link)) {
|
||||||
event(new LinkDiscovered($ejaculation->link));
|
event(new LinkDiscovered($ejaculation->link));
|
||||||
@ -180,8 +219,11 @@ class EjaculationController extends Controller
|
|||||||
$this->authorize('edit', $ejaculation);
|
$this->authorize('edit', $ejaculation);
|
||||||
|
|
||||||
$user = User::findOrFail($ejaculation->user_id);
|
$user = User::findOrFail($ejaculation->user_id);
|
||||||
$ejaculation->tags()->detach();
|
|
||||||
$ejaculation->delete();
|
DB::transaction(function () use ($ejaculation) {
|
||||||
|
$ejaculation->tags()->detach();
|
||||||
|
$ejaculation->delete();
|
||||||
|
});
|
||||||
|
|
||||||
return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。');
|
return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。');
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ class MetadataResolver implements Resolver
|
|||||||
'~www\.xtube\.com/video-watch/.*-\d+$~'=> XtubeResolver::class,
|
'~www\.xtube\.com/video-watch/.*-\d+$~'=> XtubeResolver::class,
|
||||||
'~ss\.kb10uy\.org/posts/\d+$~' => Kb10uyShortStoryServerResolver::class,
|
'~ss\.kb10uy\.org/posts/\d+$~' => Kb10uyShortStoryServerResolver::class,
|
||||||
'~www\.hentai-foundry\.com/pictures/user/.+/\d+/.+~'=> HentaiFoundryResolver::class,
|
'~www\.hentai-foundry\.com/pictures/user/.+/\d+/.+~'=> HentaiFoundryResolver::class,
|
||||||
|
'~(www\.)?((mobile|m)\.)?twitter\.com/(#!/)?[0-9a-zA-Z_]{1,15}/status(es)?/([0-9]+)/?(\\?.+)?$~' => TwitterResolver::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
public $mimeTypes = [
|
public $mimeTypes = [
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
namespace App\MetadataResolver;
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Cookie\CookieJar;
|
||||||
|
use GuzzleHttp\RequestOptions;
|
||||||
|
|
||||||
class OGPResolver implements Resolver, Parser
|
class OGPResolver implements Resolver, Parser
|
||||||
{
|
{
|
||||||
@ -18,7 +20,7 @@ class OGPResolver implements Resolver, Parser
|
|||||||
|
|
||||||
public function resolve(string $url): Metadata
|
public function resolve(string $url): Metadata
|
||||||
{
|
{
|
||||||
return $this->parse($this->client->get($url)->getBody());
|
return $this->parse($this->client->get($url, [RequestOptions::COOKIES => new CookieJar()])->getBody());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parse(string $html): Metadata
|
public function parse(string $html): Metadata
|
||||||
|
33
app/MetadataResolver/TwitterResolver.php
Normal file
33
app/MetadataResolver/TwitterResolver.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class TwitterResolver implements Resolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
/**
|
||||||
|
* @var OGPResolver
|
||||||
|
*/
|
||||||
|
private $ogpResolver;
|
||||||
|
|
||||||
|
public function __construct(Client $client, OGPResolver $ogpResolver)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
$this->ogpResolver = $ogpResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
$url = preg_replace('/(www\.)?(mobile|m)\.twitter\.com/u', 'twitter.com', $url);
|
||||||
|
|
||||||
|
$res = $this->client->get($url);
|
||||||
|
$html = (string) $res->getBody();
|
||||||
|
|
||||||
|
return $this->ogpResolver->parse($html);
|
||||||
|
}
|
||||||
|
}
|
62
app/Rules/FuzzyBoolean.php
Normal file
62
app/Rules/FuzzyBoolean.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\Rule;
|
||||||
|
|
||||||
|
class FuzzyBoolean implements Rule
|
||||||
|
{
|
||||||
|
public static function isTruthy($value): bool
|
||||||
|
{
|
||||||
|
if ($value === 1 || $value === '1') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lower = strtolower((string)$value);
|
||||||
|
|
||||||
|
return $lower === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isFalsy($value): bool
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '' || $value === 0 || $value === '0') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lower = strtolower((string)$value);
|
||||||
|
|
||||||
|
return $lower === 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new rule instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the validation rule passes.
|
||||||
|
*
|
||||||
|
* @param string $attribute
|
||||||
|
* @param mixed $value
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function passes($attribute, $value)
|
||||||
|
{
|
||||||
|
return self::isTruthy($value) || self::isFalsy($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation error message.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function message()
|
||||||
|
{
|
||||||
|
return __('validation.boolean');
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,7 @@ class CheckinCsvExporter
|
|||||||
$csv->addStreamFilter('convert.mbstring.encoding.UTF-8:SJIS-win');
|
$csv->addStreamFilter('convert.mbstring.encoding.UTF-8:SJIS-win');
|
||||||
}
|
}
|
||||||
|
|
||||||
$header = ['日時', 'ノート', 'オカズリンク'];
|
$header = ['日時', 'ノート', 'オカズリンク', '非公開', 'センシティブ'];
|
||||||
for ($i = 1; $i <= 32; $i++) {
|
for ($i = 1; $i <= 32; $i++) {
|
||||||
$header[] = "タグ{$i}";
|
$header[] = "タグ{$i}";
|
||||||
}
|
}
|
||||||
@ -45,6 +45,8 @@ class CheckinCsvExporter
|
|||||||
$ejaculation->ejaculated_date->format('Y/m/d H:i'),
|
$ejaculation->ejaculated_date->format('Y/m/d H:i'),
|
||||||
$ejaculation->note,
|
$ejaculation->note,
|
||||||
$ejaculation->link,
|
$ejaculation->link,
|
||||||
|
self::formatBoolean($ejaculation->is_private),
|
||||||
|
self::formatBoolean($ejaculation->is_too_sensitive),
|
||||||
];
|
];
|
||||||
foreach ($ejaculation->tags->take(32) as $tag) {
|
foreach ($ejaculation->tags->take(32) as $tag) {
|
||||||
$record[] = $tag->name;
|
$record[] = $tag->name;
|
||||||
@ -54,4 +56,9 @@ class CheckinCsvExporter
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function formatBoolean($value): string
|
||||||
|
{
|
||||||
|
return $value ? 'true' : 'false';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ namespace App\Services;
|
|||||||
use App\Ejaculation;
|
use App\Ejaculation;
|
||||||
use App\Exceptions\CsvImportException;
|
use App\Exceptions\CsvImportException;
|
||||||
use App\Rules\CsvDateTime;
|
use App\Rules\CsvDateTime;
|
||||||
|
use App\Rules\FuzzyBoolean;
|
||||||
use App\Tag;
|
use App\Tag;
|
||||||
use App\User;
|
use App\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
@ -75,6 +76,8 @@ class CheckinCsvImporter
|
|||||||
'日時' => ['required', new CsvDateTime()],
|
'日時' => ['required', new CsvDateTime()],
|
||||||
'ノート' => 'nullable|string|max:500',
|
'ノート' => 'nullable|string|max:500',
|
||||||
'オカズリンク' => 'nullable|url|max:2000',
|
'オカズリンク' => 'nullable|url|max:2000',
|
||||||
|
'非公開' => ['nullable', new FuzzyBoolean()],
|
||||||
|
'センシティブ' => ['nullable', new FuzzyBoolean()],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
@ -88,6 +91,12 @@ class CheckinCsvImporter
|
|||||||
$ejaculation->note = str_replace(["\r\n", "\r"], "\n", $record['ノート'] ?? '');
|
$ejaculation->note = str_replace(["\r\n", "\r"], "\n", $record['ノート'] ?? '');
|
||||||
$ejaculation->link = $record['オカズリンク'] ?? '';
|
$ejaculation->link = $record['オカズリンク'] ?? '';
|
||||||
$ejaculation->source = Ejaculation::SOURCE_CSV;
|
$ejaculation->source = Ejaculation::SOURCE_CSV;
|
||||||
|
if (isset($record['非公開'])) {
|
||||||
|
$ejaculation->is_private = FuzzyBoolean::isTruthy($record['非公開']);
|
||||||
|
}
|
||||||
|
if (isset($record['センシティブ'])) {
|
||||||
|
$ejaculation->is_too_sensitive = FuzzyBoolean::isTruthy($record['センシティブ']);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$tags = $this->parseTags($line, $record);
|
$tags = $this->parseTags($line, $record);
|
||||||
|
22
package.json
22
package.json
@ -8,31 +8,35 @@
|
|||||||
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
|
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||||
"prod": "npm run production",
|
"prod": "npm run production",
|
||||||
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||||
"eslint": "eslint --ext .js,.ts,.vue resources/",
|
"eslint": "eslint --ext .js,.ts,.tsx resources/",
|
||||||
"stylelint": "stylelint resources/assets/sass/**/*",
|
"stylelint": "stylelint resources/assets/sass/**/*",
|
||||||
"doc": "redoc-cli bundle -o public/apidoc.html openapi.yaml"
|
"doc": "redoc-cli bundle -o public/apidoc.html openapi.yaml"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bootstrap": "^4.5.0",
|
"@types/bootstrap": "^4.5.0",
|
||||||
"@types/cal-heatmap": "^3.3.10",
|
"@types/cal-heatmap": "^3.3.10",
|
||||||
"@types/chart.js": "^2.9.22",
|
"@types/chart.js": "^2.9.23",
|
||||||
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/clipboard": "^2.0.1",
|
"@types/clipboard": "^2.0.1",
|
||||||
"@types/jquery": "^3.3.38",
|
"@types/jquery": "^3.3.38",
|
||||||
"@types/js-cookie": "^2.2.0",
|
"@types/js-cookie": "^2.2.0",
|
||||||
"@types/qs": "^6.9.4",
|
"@types/qs": "^6.9.4",
|
||||||
|
"@types/react": "^16.9.35",
|
||||||
|
"@types/react-dom": "^16.9.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.1.0",
|
"@typescript-eslint/eslint-plugin": "^3.1.0",
|
||||||
"@typescript-eslint/parser": "^3.1.0",
|
"@typescript-eslint/parser": "^3.1.0",
|
||||||
"bootstrap": "^4.5.0",
|
"bootstrap": "^4.5.0",
|
||||||
"cal-heatmap": "^3.3.10",
|
"cal-heatmap": "^3.3.10",
|
||||||
"chart.js": "^2.7.1",
|
"chart.js": "^2.7.1",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
"clipboard": "^2.0.6",
|
"clipboard": "^2.0.6",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"date-fns": "^1.30.1",
|
"date-fns": "^2.15.0",
|
||||||
"eslint": "^7.6.0",
|
"eslint": "^7.6.0",
|
||||||
"eslint-config-prettier": "^6.11.0",
|
"eslint-config-prettier": "^6.11.0",
|
||||||
"eslint-plugin-jquery": "^1.5.1",
|
"eslint-plugin-jquery": "^1.5.1",
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-react": "^7.20.6",
|
||||||
"husky": "^1.3.1",
|
"husky": "^1.3.1",
|
||||||
"jquery": "^3.5.0",
|
"jquery": "^3.5.0",
|
||||||
"js-cookie": "^2.2.0",
|
"js-cookie": "^2.2.0",
|
||||||
@ -44,17 +48,15 @@
|
|||||||
"prettier": "^2.0.5",
|
"prettier": "^2.0.5",
|
||||||
"redoc-cli": "^0.9.8",
|
"redoc-cli": "^0.9.8",
|
||||||
"qs": "^6.9.4",
|
"qs": "^6.9.4",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"react-dom": "^16.13.1",
|
||||||
"resolve-url-loader": "^3.1.1",
|
"resolve-url-loader": "^3.1.1",
|
||||||
"sass": "^1.26.8",
|
"sass": "^1.26.8",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"stylelint": "^9.10.1",
|
"stylelint": "^9.10.1",
|
||||||
"stylelint-config-recess-order": "^2.0.4",
|
"stylelint-config-recess-order": "^2.0.4",
|
||||||
"ts-loader": "^6.0.1",
|
"ts-loader": "^6.0.1",
|
||||||
"typescript": "^3.4.5",
|
"typescript": "^3.4.5"
|
||||||
"vue": "^2.6.10",
|
|
||||||
"vue-class-component": "^7.1.0",
|
|
||||||
"vue-property-decorator": "^9.0.0",
|
|
||||||
"vue-template-compiler": "^2.6.10"
|
|
||||||
},
|
},
|
||||||
"stylelint": {
|
"stylelint": {
|
||||||
"extends": "stylelint-config-recess-order"
|
"extends": "stylelint-config-recess-order"
|
||||||
@ -69,7 +71,7 @@
|
|||||||
"stylelint --fix",
|
"stylelint --fix",
|
||||||
"git add"
|
"git add"
|
||||||
],
|
],
|
||||||
"*.{ts,js,vue}": [
|
"*.{ts,tsx,js}": [
|
||||||
"eslint --fix",
|
"eslint --fix",
|
||||||
"git add"
|
"git add"
|
||||||
],
|
],
|
||||||
|
12
resources/assets/js/@types/jquery-tissue.d.ts
vendored
12
resources/assets/js/@types/jquery-tissue.d.ts
vendored
@ -1,12 +0,0 @@
|
|||||||
// tissue.ts で定義されているjQuery Pluginの型定義
|
|
||||||
declare namespace JQueryTissue {
|
|
||||||
interface LinkCardOptions {
|
|
||||||
endpoint: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface JQuery<TElement = HTMLElement> {
|
|
||||||
linkCard: (options?: JQueryTissue.LinkCardOptions) => this;
|
|
||||||
pageSelector: () => this;
|
|
||||||
deleteCheckinModal: () => this;
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
import * as Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { fetchPostJson, fetchDeleteJson, ResponseError } from './fetch';
|
import { fetchPostJson, fetchDeleteJson, ResponseError } from './fetch';
|
||||||
|
import { linkCard, pageSelector, deleteCheckinModal } from './tissue';
|
||||||
|
|
||||||
require('./bootstrap');
|
require('./bootstrap');
|
||||||
|
|
||||||
@ -20,14 +21,18 @@ $(() => {
|
|||||||
}
|
}
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
$('.alert').alert();
|
$('.alert').alert();
|
||||||
$('.tis-page-selector').pageSelector();
|
document.querySelectorAll('.tis-page-selector').forEach(pageSelector);
|
||||||
|
|
||||||
$('.link-card').linkCard();
|
document.querySelectorAll('.link-card').forEach(linkCard);
|
||||||
const $deleteCheckinModal = $('#deleteCheckinModal').deleteCheckinModal();
|
|
||||||
$(document).on('click', '[data-target="#deleteCheckinModal"]', function (event) {
|
const elDeleteCheckinModal = document.getElementById('deleteCheckinModal');
|
||||||
event.preventDefault();
|
if (elDeleteCheckinModal) {
|
||||||
$deleteCheckinModal.modal('show', this);
|
const $deleteCheckinModal = deleteCheckinModal(elDeleteCheckinModal);
|
||||||
});
|
$(document).on('click', '[data-target="#deleteCheckinModal"]', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
$deleteCheckinModal.modal('show', this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$(document).on('click', '[data-href]', function (_event) {
|
$(document).on('click', '[data-href]', function (_event) {
|
||||||
location.href = $(this).data('href');
|
location.href = $(this).data('href');
|
||||||
|
@ -1,5 +1,2 @@
|
|||||||
// jQuery
|
|
||||||
import './tissue';
|
|
||||||
|
|
||||||
// Bootstrap
|
// Bootstrap
|
||||||
import 'bootstrap';
|
import 'bootstrap';
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
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;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
6
resources/assets/js/checkin.tsx
Normal file
6
resources/assets/js/checkin.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
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'));
|
25
resources/assets/js/components/CheckBox.tsx
Normal file
25
resources/assets/js/components/CheckBox.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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>
|
||||||
|
);
|
149
resources/assets/js/components/CheckinForm.tsx
Normal file
149
resources/assets/js/components/CheckinForm.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
16
resources/assets/js/components/FieldError.tsx
Normal file
16
resources/assets/js/components/FieldError.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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;
|
157
resources/assets/js/components/MetadataPreview.tsx
Normal file
157
resources/assets/js/components/MetadataPreview.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
|
75
resources/assets/js/components/TagInput.tsx
Normal file
75
resources/assets/js/components/TagInput.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
|
@ -1,4 +1,4 @@
|
|||||||
import * as Chart from 'chart.js';
|
import Chart from 'chart.js';
|
||||||
|
|
||||||
const graph = document.getElementById('global-count-graph') as HTMLCanvasElement;
|
const graph = document.getElementById('global-count-graph') as HTMLCanvasElement;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
@ -1,80 +1,73 @@
|
|||||||
import { fetchGet } from './fetch';
|
import { fetchGet } from './fetch';
|
||||||
|
|
||||||
(function ($) {
|
export function suicide<T>(e: T) {
|
||||||
$.fn.linkCard = function (options) {
|
return function (): never {
|
||||||
const settings = $.extend(
|
throw e;
|
||||||
{
|
};
|
||||||
endpoint: '/api/checkin/card',
|
}
|
||||||
},
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.each(function () {
|
const die = suicide('Element not found!');
|
||||||
const $this = $(this);
|
|
||||||
|
|
||||||
const url = $this.find('a').attr('href');
|
export function linkCard(el: Element) {
|
||||||
if (!url) {
|
const url = el.querySelector('a')?.href;
|
||||||
return;
|
if (!url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchGet('/api/checkin/card', { url })
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
const metaColumn = el.querySelector('.col-12:last-of-type') || die();
|
||||||
|
const imageColumn = el.querySelector<HTMLElement>('.col-12:first-of-type') || die();
|
||||||
|
const title = el.querySelector<HTMLElement>('.card-title') || die();
|
||||||
|
const desc = el.querySelector<HTMLElement>('.card-text') || die();
|
||||||
|
const image = imageColumn.querySelector('img') || die();
|
||||||
|
|
||||||
|
if (data.title === '') {
|
||||||
|
title.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
title.textContent = data.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchGet(settings.endpoint, { url })
|
if (data.description === '') {
|
||||||
.then((response) => response.json())
|
desc.style.display = 'none';
|
||||||
.then((data) => {
|
} else {
|
||||||
const $metaColumn = $this.find('.col-12:last-of-type');
|
desc.textContent = data.description;
|
||||||
const $imageColumn = $this.find('.col-12:first-of-type');
|
}
|
||||||
const $title = $this.find('.card-title');
|
|
||||||
const $desc = $this.find('.card-text');
|
|
||||||
const $image = $imageColumn.find('img');
|
|
||||||
|
|
||||||
if (data.title === '') {
|
if (data.image === '') {
|
||||||
$title.hide();
|
imageColumn.style.display = 'none';
|
||||||
} else {
|
metaColumn.classList.remove('col-md-6');
|
||||||
$title.text(data.title);
|
} else {
|
||||||
}
|
image.src = data.image;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.description === '') {
|
if (data.title !== '' || data.description !== '' || data.image !== '') {
|
||||||
$desc.hide();
|
el.classList.remove('d-none');
|
||||||
} else {
|
}
|
||||||
$desc.text(data.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.image === '') {
|
|
||||||
$imageColumn.hide();
|
|
||||||
$metaColumn.removeClass('col-md-6');
|
|
||||||
} else {
|
|
||||||
$image.attr('src', data.image);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.title !== '' || data.description !== '' || data.image !== '') {
|
|
||||||
$this.removeClass('d-none');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.pageSelector = function () {
|
return el;
|
||||||
return this.on('change', function () {
|
}
|
||||||
location.href = $(this).find(':selected').data('href');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$.fn.deleteCheckinModal = function () {
|
export function pageSelector(el: Element) {
|
||||||
return this.each(function () {
|
if (el instanceof HTMLSelectElement) {
|
||||||
$(this)
|
el.addEventListener('change', function () {
|
||||||
.on('show.bs.modal', function (event) {
|
location.href = this.options[this.selectedIndex].dataset.href as string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const target = $(event.relatedTarget!);
|
|
||||||
const modal = $(this);
|
|
||||||
modal.find('.modal-body .date-label').text(target.data('date'));
|
|
||||||
modal.data('id', target.data('id'));
|
|
||||||
})
|
|
||||||
.find('.btn-danger')
|
|
||||||
.on('click', function (_event) {
|
|
||||||
const modal = $('#deleteCheckinModal');
|
|
||||||
const form = modal.find('form');
|
|
||||||
form.attr('action', form.attr('action')?.replace('@', modal.data('id')) || null);
|
|
||||||
form.submit();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
})(jQuery);
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCheckinModal(modal: Element) {
|
||||||
|
let id: any = null;
|
||||||
|
modal.querySelector('form')?.addEventListener('submit', function () {
|
||||||
|
this.action = this.action.replace('@', id);
|
||||||
|
});
|
||||||
|
return $(modal).on('show.bs.modal', function (event) {
|
||||||
|
const target = event.relatedTarget || die();
|
||||||
|
const dateLabel = this.querySelector('.modal-body .date-label') || die();
|
||||||
|
dateLabel.textContent = target.dataset.date || null;
|
||||||
|
id = target.dataset.id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as CalHeatMap from 'cal-heatmap';
|
import CalHeatMap from 'cal-heatmap';
|
||||||
import { subMonths } from 'date-fns';
|
import { subMonths } from 'date-fns';
|
||||||
|
|
||||||
if (document.getElementById('cal-heatmap')) {
|
if (document.getElementById('cal-heatmap')) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as CalHeatMap from 'cal-heatmap';
|
import CalHeatMap from 'cal-heatmap';
|
||||||
import * as Chart from 'chart.js';
|
import Chart from 'chart.js';
|
||||||
import { addMonths, format } from 'date-fns';
|
import { addMonths, format } from 'date-fns';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// 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++) {
|
for (let i = 0; i < 12; i++) {
|
||||||
const current = addMonths(from, i);
|
const current = addMonths(from, i);
|
||||||
const yearAndMonth = format(current, 'YYYY/MM');
|
const yearAndMonth = format(current, 'yyyy/MM');
|
||||||
keys.push(yearAndMonth);
|
keys.push(yearAndMonth);
|
||||||
values.push(graphData.monthlySum[yearAndMonth] || 0);
|
values.push(graphData.monthlySum[yearAndMonth] || 0);
|
||||||
}
|
}
|
||||||
|
2
resources/assets/sass/app.scss
vendored
2
resources/assets/sass/app.scss
vendored
@ -13,6 +13,8 @@ $primary: #e53fb1;
|
|||||||
// Components
|
// Components
|
||||||
@import "components/ejaculation";
|
@import "components/ejaculation";
|
||||||
@import "components/link-card";
|
@import "components/link-card";
|
||||||
|
@import "components/tag-input";
|
||||||
|
@import "components/metadata-preview";
|
||||||
|
|
||||||
// Tag
|
// Tag
|
||||||
@import "tag/index";
|
@import "tag/index";
|
||||||
|
32
resources/assets/sass/components/_metadata-preview.scss
vendored
Normal file
32
resources/assets/sass/components/_metadata-preview.scss
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
.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
Normal file
11
resources/assets/sass/components/_tag-input.scss
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.tis-tag-input {
|
||||||
|
&-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-field {
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,99 +3,15 @@
|
|||||||
@section('title', 'チェックイン')
|
@section('title', 'チェックイン')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div id="app" class="container">
|
<div class="container">
|
||||||
<h2>今致してる?</h2>
|
<h2>今致してる?</h2>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="row justify-content-center mt-5">
|
<div class="row justify-content-center mt-5">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<form method="post" action="{{ route('checkin') }}">
|
<form method="post" action="{{ route('checkin') }}">
|
||||||
{{ csrf_field() }}
|
{{ csrf_field() }}
|
||||||
|
<div id="checkinForm">
|
||||||
<div class="form-row">
|
<div class="text-center small" style="height: 640px;">しばらくお待ちください…</div>
|
||||||
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<p class="text-center small mt-4"><strong>Tips</strong>: ブックマークレットや共有機能で、簡単にチェックインできます! <a href="{{ route('checkin.tools') }}" target="_blank" rel="noopener">使い方はこちら</a></p>
|
<p class="text-center small mt-4"><strong>Tips</strong>: ブックマークレットや共有機能で、簡単にチェックインできます! <a href="{{ route('checkin.tools') }}" target="_blank" rel="noopener">使い方はこちら</a></p>
|
||||||
@ -105,5 +21,6 @@
|
|||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@push('script')
|
@push('script')
|
||||||
|
<script id="initialState" type="application/json">@json($initialState)</script>
|
||||||
<script src="{{ mix('js/checkin.js') }}"></script>
|
<script src="{{ mix('js/checkin.js') }}"></script>
|
||||||
@endpush
|
@endpush
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
@section('title', 'チェックインの修正')
|
@section('title', 'チェックインの修正')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div id="app" class="container">
|
<div class="container">
|
||||||
<h2>チェックインの修正</h2>
|
<h2>チェックインの修正</h2>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="row justify-content-center mt-5">
|
<div class="row justify-content-center mt-5">
|
||||||
@ -11,92 +11,8 @@
|
|||||||
<form method="post" action="{{ route('checkin.update', ['id' => $ejaculation->id]) }}">
|
<form method="post" action="{{ route('checkin.update', ['id' => $ejaculation->id]) }}">
|
||||||
{{ method_field('PUT') }}
|
{{ method_field('PUT') }}
|
||||||
{{ csrf_field() }}
|
{{ csrf_field() }}
|
||||||
|
<div id="checkinForm">
|
||||||
<div class="form-row">
|
<div class="text-center small" style="height: 640px;">しばらくお待ちください…</div>
|
||||||
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -105,5 +21,6 @@
|
|||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@push('script')
|
@push('script')
|
||||||
|
<script id="initialState" type="application/json">@json($initialState)</script>
|
||||||
<script src="{{ mix('js/checkin.js') }}"></script>
|
<script src="{{ mix('js/checkin.js') }}"></script>
|
||||||
@endpush
|
@endpush
|
||||||
|
@ -102,13 +102,13 @@
|
|||||||
削除確認
|
削除確認
|
||||||
@endslot
|
@endslot
|
||||||
<span class="date-label"></span> のチェックインを削除してもよろしいですか?
|
<span class="date-label"></span> のチェックインを削除してもよろしいですか?
|
||||||
<form action="{{ route('checkin.destroy', ['id' => '@']) }}" method="post">
|
|
||||||
{{ csrf_field() }}
|
|
||||||
{{ method_field('DELETE') }}
|
|
||||||
</form>
|
|
||||||
@slot('footer')
|
@slot('footer')
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">キャンセル</button>
|
<form action="{{ route('checkin.destroy', ['id' => '@']) }}" method="post">
|
||||||
<button type="button" class="btn btn-danger">削除</button>
|
{{ 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
|
@endslot
|
||||||
@endcomponent
|
@endcomponent
|
||||||
@endsection
|
@endsection
|
||||||
|
@ -122,13 +122,13 @@
|
|||||||
削除確認
|
削除確認
|
||||||
@endslot
|
@endslot
|
||||||
<span class="date-label"></span> のチェックインを削除してもよろしいですか?
|
<span class="date-label"></span> のチェックインを削除してもよろしいですか?
|
||||||
<form action="{{ route('checkin.destroy', ['id' => '@']) }}" method="post">
|
|
||||||
{{ csrf_field() }}
|
|
||||||
{{ method_field('DELETE') }}
|
|
||||||
</form>
|
|
||||||
@slot('footer')
|
@slot('footer')
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">キャンセル</button>
|
<form action="{{ route('checkin.destroy', ['id' => '@']) }}" method="post">
|
||||||
<button type="button" class="btn btn-danger">削除</button>
|
{{ 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
|
@endslot
|
||||||
@endcomponent
|
@endcomponent
|
||||||
@endsection
|
@endsection
|
||||||
|
@ -277,6 +277,48 @@ class CheckinCsvImporterTest extends TestCase
|
|||||||
$this->assertEquals(Ejaculation::SOURCE_CSV, $ejaculation->source);
|
$this->assertEquals(Ejaculation::SOURCE_CSV, $ejaculation->source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testIsPrivateUTF8()
|
||||||
|
{
|
||||||
|
$user = factory(User::class)->create();
|
||||||
|
|
||||||
|
$importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/private.utf8.csv');
|
||||||
|
$importer->execute();
|
||||||
|
|
||||||
|
$ejaculations = $user->ejaculations()->orderBy('ejaculated_date')->get();
|
||||||
|
|
||||||
|
$this->assertSame(9, $ejaculations->count());
|
||||||
|
$this->assertTrue($ejaculations[0]->is_private);
|
||||||
|
$this->assertTrue($ejaculations[1]->is_private);
|
||||||
|
$this->assertTrue($ejaculations[2]->is_private);
|
||||||
|
$this->assertTrue($ejaculations[3]->is_private);
|
||||||
|
$this->assertFalse($ejaculations[4]->is_private);
|
||||||
|
$this->assertFalse($ejaculations[5]->is_private);
|
||||||
|
$this->assertFalse($ejaculations[6]->is_private);
|
||||||
|
$this->assertFalse($ejaculations[7]->is_private);
|
||||||
|
$this->assertFalse($ejaculations[8]->is_private);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsTooSensitiveUTF8()
|
||||||
|
{
|
||||||
|
$user = factory(User::class)->create();
|
||||||
|
|
||||||
|
$importer = new CheckinCsvImporter($user, __DIR__ . '/../../fixture/Csv/too-sensitive.utf8.csv');
|
||||||
|
$importer->execute();
|
||||||
|
|
||||||
|
$ejaculations = $user->ejaculations()->orderBy('ejaculated_date')->get();
|
||||||
|
|
||||||
|
$this->assertSame(9, $ejaculations->count());
|
||||||
|
$this->assertTrue($ejaculations[0]->is_too_sensitive);
|
||||||
|
$this->assertTrue($ejaculations[1]->is_too_sensitive);
|
||||||
|
$this->assertTrue($ejaculations[2]->is_too_sensitive);
|
||||||
|
$this->assertTrue($ejaculations[3]->is_too_sensitive);
|
||||||
|
$this->assertFalse($ejaculations[4]->is_too_sensitive);
|
||||||
|
$this->assertFalse($ejaculations[5]->is_too_sensitive);
|
||||||
|
$this->assertFalse($ejaculations[6]->is_too_sensitive);
|
||||||
|
$this->assertFalse($ejaculations[7]->is_too_sensitive);
|
||||||
|
$this->assertFalse($ejaculations[8]->is_too_sensitive);
|
||||||
|
}
|
||||||
|
|
||||||
public function testDontThrowUniqueKeyViolation()
|
public function testDontThrowUniqueKeyViolation()
|
||||||
{
|
{
|
||||||
$user = factory(User::class)->create();
|
$user = factory(User::class)->create();
|
||||||
|
10
tests/fixture/Csv/private.utf8.csv
vendored
Normal file
10
tests/fixture/Csv/private.utf8.csv
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
日時,非公開
|
||||||
|
2020/01/23 06:01:00,true
|
||||||
|
2020/01/23 06:02:00,TRUE
|
||||||
|
2020/01/23 06:03:00,True
|
||||||
|
2020/01/23 06:04:00,1
|
||||||
|
2020/01/23 07:01:00,false
|
||||||
|
2020/01/23 07:02:00,FALSE
|
||||||
|
2020/01/23 07:03:00,False
|
||||||
|
2020/01/23 07:04:00,0
|
||||||
|
2020/01/23 07:05:00,
|
|
10
tests/fixture/Csv/too-sensitive.utf8.csv
vendored
Normal file
10
tests/fixture/Csv/too-sensitive.utf8.csv
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
日時,センシティブ
|
||||||
|
2020/01/23 06:01:00,true
|
||||||
|
2020/01/23 06:02:00,TRUE
|
||||||
|
2020/01/23 06:03:00,True
|
||||||
|
2020/01/23 06:04:00,1
|
||||||
|
2020/01/23 07:01:00,false
|
||||||
|
2020/01/23 07:02:00,FALSE
|
||||||
|
2020/01/23 07:03:00,False
|
||||||
|
2020/01/23 07:04:00,0
|
||||||
|
2020/01/23 07:05:00,
|
|
@ -5,9 +5,11 @@
|
|||||||
"module": "es2015",
|
"module": "es2015",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"resources/assets/js/**/*"
|
"resources/assets/js/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
2
webpack.mix.js
vendored
2
webpack.mix.js
vendored
@ -21,7 +21,7 @@ mix.ts('resources/assets/js/app.ts', 'public/js')
|
|||||||
.ts('resources/assets/js/setting/import.ts', 'public/js/setting')
|
.ts('resources/assets/js/setting/import.ts', 'public/js/setting')
|
||||||
.ts('resources/assets/js/setting/deactivate.ts', 'public/js/setting')
|
.ts('resources/assets/js/setting/deactivate.ts', 'public/js/setting')
|
||||||
.ts('resources/assets/js/setting/webhooks.ts', 'public/js/setting')
|
.ts('resources/assets/js/setting/webhooks.ts', 'public/js/setting')
|
||||||
.ts('resources/assets/js/checkin.ts', 'public/js')
|
.ts('resources/assets/js/checkin.tsx', 'public/js')
|
||||||
.sass('resources/assets/sass/app.scss', 'public/css')
|
.sass('resources/assets/sass/app.scss', 'public/css')
|
||||||
.autoload({
|
.autoload({
|
||||||
jquery: ['$', 'jQuery', 'window.jQuery'],
|
jquery: ['$', 'jQuery', 'window.jQuery'],
|
||||||
|
Loading…
Reference in New Issue
Block a user