Merge pull request #431 from shikorism/develop

Release 20200709.0040
This commit is contained in:
shibafu 2020-07-09 00:42:58 +09:00 committed by GitHub
commit 65cb2127ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 2347 additions and 857 deletions

View File

@ -105,6 +105,12 @@ jobs:
command: yarn run stylelint command: yarn run stylelint
when: always when: always
# Run eslint
- run:
name: eslint
command: yarn run eslint
when: always
# Run unit test # Run unit test
- run: - run:
command: | command: |

28
.eslintrc.js Normal file
View File

@ -0,0 +1,28 @@
module.exports = {
env: {
browser: true,
es2020: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:vue/essential',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/@typescript-eslint',
'prettier/vue',
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 11,
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: ['prettier', 'vue', '@typescript-eslint'],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
},
};

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
/public/hot /public/hot
/public/storage /public/storage
/public/mix-manifest.json /public/mix-manifest.json
/public/report.html
/storage/*.key /storage/*.key
/vendor /vendor
/.idea /.idea

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"arrowParens": "always",
"singleQuote": true,
"printWidth": 120
}

View File

@ -3,6 +3,7 @@
namespace App\MetadataResolver; namespace App\MetadataResolver;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\DomCrawler\Crawler;
class FanzaResolver implements Resolver class FanzaResolver implements Resolver
@ -43,7 +44,9 @@ class FanzaResolver implements Resolver
public function resolve(string $url): Metadata public function resolve(string $url): Metadata
{ {
$res = $this->client->get($url); $cookieJar = CookieJar::fromArray(['age_check_done' => '1'], 'dmm.co.jp');
$res = $this->client->get($url, ['cookies' => $cookieJar]);
$html = (string) $res->getBody(); $html = (string) $res->getBody();
$crawler = new Crawler($html); $crawler = new Crawler($html);

View File

@ -26,7 +26,7 @@ class MetadataResolver implements Resolver
'~dmm\.co\.jp/~' => FanzaResolver::class, '~dmm\.co\.jp/~' => FanzaResolver::class,
'~www\.patreon\.com/~' => PatreonResolver::class, '~www\.patreon\.com/~' => PatreonResolver::class,
'~www\.deviantart\.com/.*/art/.*~' => DeviantArtResolver::class, '~www\.deviantart\.com/.*/art/.*~' => DeviantArtResolver::class,
'~\.syosetu\.com/n\d+[a-z]{2,}~' => NarouResolver::class, '~\.syosetu\.com/n\d+[a-z]+~' => NarouResolver::class,
'~ci-en\.(jp|net|dlsite\.com)/creator/\d+/article/\d+~' => CienResolver::class, '~ci-en\.(jp|net|dlsite\.com)/creator/\d+/article/\d+~' => CienResolver::class,
'~www\.plurk\.com\/p\/.*~' => PlurkResolver::class, '~www\.plurk\.com\/p\/.*~' => PlurkResolver::class,
'~(adult\.)?contents\.fc2\.com\/article_search\.php\?id=\d+~' => FC2ContentsResolver::class, '~(adult\.)?contents\.fc2\.com\/article_search\.php\?id=\d+~' => FC2ContentsResolver::class,

1132
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,15 +8,26 @@
"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/",
"stylelint": "stylelint resources/assets/sass/**/*" "stylelint": "stylelint resources/assets/sass/**/*"
}, },
"devDependencies": { "devDependencies": {
"@types/bootstrap": "^4.5.0",
"@types/cal-heatmap": "^3.3.10",
"@types/chart.js": "^2.9.22",
"@types/jquery": "^3.3.38", "@types/jquery": "^3.3.38",
"bootstrap": "^4.3.1", "@types/js-cookie": "^2.2.0",
"@typescript-eslint/eslint-plugin": "^3.1.0",
"@typescript-eslint/parser": "^3.1.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",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"date-fns": "^1.30.1", "date-fns": "^1.30.1",
"eslint": "^7.2.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^6.2.2",
"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",
@ -25,16 +36,17 @@
"lint-staged": "^8.1.5", "lint-staged": "^8.1.5",
"open-iconic": "^1.1.1", "open-iconic": "^1.1.1",
"popper.js": "^1.14.7", "popper.js": "^1.14.7",
"resolve-url-loader": "^2.3.1", "prettier": "^2.0.5",
"sass": "^1.17.0", "resolve-url-loader": "^3.1.1",
"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.1", "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": "^2.6.10",
"vue-class-component": "^7.1.0", "vue-class-component": "^7.1.0",
"vue-property-decorator": "^8.1.1", "vue-property-decorator": "^9.0.0",
"vue-template-compiler": "^2.6.10" "vue-template-compiler": "^2.6.10"
}, },
"stylelint": { "stylelint": {
@ -50,6 +62,10 @@
"stylelint --fix", "stylelint --fix",
"git add" "git add"
], ],
"*.{ts,js,vue}" : [
"eslint --fix",
"git add"
],
"*.php": [ "*.php": [
"composer fix", "composer fix",
"git add" "git add"

View File

@ -0,0 +1,4 @@
// なんか @types/cal-heatmap 入れても動かなかったんですけど!!
declare module 'cal-heatmap' {
export = CalHeatMap;
}

View File

@ -0,0 +1,4 @@
// @types/bootstrap に足りないもの
interface JQuery<TElement = HTMLElement> {
modal(action: 'toggle' | 'show' | 'hide' | 'handleUpdate' | 'dispose', relatedTarget?: TElement): this;
}

View File

@ -0,0 +1,12 @@
// tissue.ts で定義されているjQuery Pluginの型定義
declare namespace JQueryTissue {
interface LinkCardOptions {
endpoint: string;
}
}
interface JQuery<TElement = HTMLElement> {
linkCard: (options?: JQueryTissue.LinkCardOptions) => this;
pageSelector: () => this;
deleteCheckinModal: () => this;
}

View File

@ -0,0 +1,4 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

View File

@ -1,4 +1,5 @@
import Cookies from 'js-cookie'; import * as Cookies from 'js-cookie';
import jqXHR = JQuery.jqXHR;
require('./bootstrap'); require('./bootstrap');
@ -7,10 +8,10 @@ $(() => {
$('body').removeClass('tis-need-agecheck'); $('body').removeClass('tis-need-agecheck');
} else { } else {
$('#ageCheckModal') $('#ageCheckModal')
.modal({backdrop: 'static'}) .modal({ backdrop: 'static' })
.on('hide.bs.modal', function () { .on('hide.bs.modal', function () {
$('body').removeClass('tis-need-agecheck'); $('body').removeClass('tis-need-agecheck');
Cookies.set('agechecked', '1', {expires: 365}); Cookies.set('agechecked', '1', { expires: 365 });
}); });
} }
@ -28,7 +29,7 @@ $(() => {
$deleteCheckinModal.modal('show', this); $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');
}); });
@ -40,7 +41,7 @@ $(() => {
const isLiked = $this.data('liked'); const isLiked = $this.data('liked');
if (isLiked) { if (isLiked) {
const callback = (data) => { const callback = (data: any) => {
$this.data('liked', false); $this.data('liked', false);
$this.find('.oi-heart').removeClass('text-danger'); $this.find('.oi-heart').removeClass('text-danger');
@ -51,10 +52,10 @@ $(() => {
$.ajax({ $.ajax({
url: '/api/likes/' + encodeURIComponent(targetId), url: '/api/likes/' + encodeURIComponent(targetId),
method: 'delete', method: 'delete',
type: 'json' type: 'json',
}) })
.then(callback) .then(callback)
.catch(function (xhr) { .catch(function (xhr: jqXHR) {
if (xhr.status === 404) { if (xhr.status === 404) {
callback(JSON.parse(xhr.responseText)); callback(JSON.parse(xhr.responseText));
return; return;
@ -64,7 +65,7 @@ $(() => {
alert('いいねを解除できませんでした。'); alert('いいねを解除できませんでした。');
}); });
} else { } else {
const callback = (data) => { const callback = (data: any) => {
$this.data('liked', true); $this.data('liked', true);
$this.find('.oi-heart').addClass('text-danger'); $this.find('.oi-heart').addClass('text-danger');
@ -77,11 +78,11 @@ $(() => {
method: 'post', method: 'post',
type: 'json', type: 'json',
data: { data: {
id: targetId id: targetId,
} },
}) })
.then(callback) .then(callback)
.catch(function (xhr) { .catch(function (xhr: jqXHR) {
if (xhr.status === 409) { if (xhr.status === 409) {
callback(JSON.parse(xhr.responseText)); callback(JSON.parse(xhr.responseText));
return; return;
@ -96,9 +97,9 @@ $(() => {
} }
}); });
$(document).on('click', '.card-spoiler-overlay', function (event) { $(document).on('click', '.card-spoiler-overlay', function (_event) {
const $this = $(this); const $this = $(this);
$this.siblings(".card-link").removeClass("card-spoiler"); $this.siblings('.card-link').removeClass('card-spoiler');
$this.remove(); $this.remove();
}); });
}); });

View File

@ -2,16 +2,16 @@
import './tissue'; import './tissue';
// Setup global request header // Setup global request header
const token = document.head.querySelector('meta[name="csrf-token"]'); const token = document.head.querySelector<HTMLMetaElement>('meta[name="csrf-token"]');
if (!token) { if (!token) {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
} else {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': token.content,
},
});
} }
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': token.content
}
});
// Bootstrap // Bootstrap
import 'bootstrap'; import 'bootstrap';

View File

@ -1,8 +1,8 @@
import Vue from 'vue'; import Vue from 'vue';
import TagInput from "./components/TagInput.vue"; import TagInput from './components/TagInput.vue';
import MetadataPreview from './components/MetadataPreview.vue'; import MetadataPreview from './components/MetadataPreview.vue';
export const bus = new Vue({name: "EventBus"}); export const bus = new Vue({ name: 'EventBus' });
export enum MetadataLoadState { export enum MetadataLoadState {
Inactive, Inactive,
@ -19,11 +19,11 @@ new Vue({
}, },
components: { components: {
TagInput, TagInput,
MetadataPreview MetadataPreview,
}, },
mounted() { mounted() {
// オカズリンクにURLがセットされている場合は、すぐにメタデータを取得する // オカズリンクにURLがセットされている場合は、すぐにメタデータを取得する
const linkInput = this.$el.querySelector<HTMLInputElement>("#link"); const linkInput = this.$el.querySelector<HTMLInputElement>('#link');
if (linkInput && /^https?:\/\//.test(linkInput.value)) { if (linkInput && /^https?:\/\//.test(linkInput.value)) {
this.fetchMetadata(linkInput.value); this.fetchMetadata(linkInput.value);
} }
@ -52,15 +52,17 @@ new Vue({
method: 'get', method: 'get',
type: 'json', type: 'json',
data: { data: {
url url,
} },
}).then(data => { })
this.metadata = data; .then((data) => {
this.metadataLoadState = MetadataLoadState.Success; this.metadata = data;
}).catch(e => { this.metadataLoadState = MetadataLoadState.Success;
this.metadata = null; })
this.metadataLoadState = MetadataLoadState.Failed; .catch((_e) => {
}); this.metadata = null;
} this.metadataLoadState = MetadataLoadState.Failed;
} });
},
},
}); });

View File

@ -5,23 +5,34 @@
<div v-if="state === MetadataLoadState.Loading" class="row no-gutters"> <div v-if="state === MetadataLoadState.Loading" class="row no-gutters">
<div class="col-12"> <div class="col-12">
<div class="card-body"> <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> <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>
</div> </div>
<div v-else-if="state === MetadataLoadState.Success" class="row no-gutters"> <div v-else-if="state === MetadataLoadState.Success" class="row no-gutters">
<div v-if="hasImage" class="col-4 justify-content-center align-items-center"> <div v-if="hasImage" class="col-4 justify-content-center align-items-center">
<img :src="metadata.image" alt="Thumbnail" class="w-100 bg-secondary"> <img :src="metadata.image" alt="Thumbnail" class="w-100 bg-secondary" />
</div> </div>
<div :class="descClasses"> <div :class="descClasses">
<div class="card-body"> <div class="card-body">
<h6 class="card-title font-weight-bold" style="font-size: small;">{{ metadata.title }}</h6> <h6 class="card-title font-weight-bold" style="font-size: small;">{{ metadata.title }}</h6>
<template v-if="suggestions.length > 0"> <template v-if="suggestions.length > 0">
<p class="card-text mb-2" style="font-size: small;">タグ候補<br><span class="text-secondary">(クリックするとタグ入力欄にコピーできます)</span></p> <p class="card-text mb-2" style="font-size: small;">
タグ候補<br /><span class="text-secondary"
>(クリックするとタグ入力欄にコピーできます)</span
>
</p>
<ul class="list-inline d-inline"> <ul class="list-inline d-inline">
<li v-for="tag in suggestions" <li
v-for="tag in suggestions"
:class="tagClasses(tag)" :class="tagClasses(tag)"
@click="addTag(tag.name)"><span class="oi oi-tag"></span> {{ tag.name }}</li> @click="addTag(tag.name)"
:key="tag.name"
>
<span class="oi oi-tag"></span> {{ tag.name }}
</li>
</ul> </ul>
</template> </template>
</div> </div>
@ -30,7 +41,9 @@
<div v-else class="row no-gutters"> <div v-else class="row no-gutters">
<div class="col-12"> <div class="col-12">
<div class="card-body"> <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> <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>
@ -40,104 +53,104 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {Vue, Component, Prop} from "vue-property-decorator"; import { Vue, Component, Prop } from 'vue-property-decorator';
import {bus, MetadataLoadState} from "../checkin"; import { bus, MetadataLoadState } from '../checkin';
type Metadata = { type Metadata = {
url: string, url: string;
title: string, title: string;
description: string, description: string;
image: string, image: string;
expires_at: string | null, expires_at: string | null;
tags: { tags: {
name: string name: string;
}[], }[];
}; };
type Suggestion = { type Suggestion = {
name: string, name: string;
used: boolean, 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');
} }
@Component addTag(tag: string): void {
export default class MetadataPreview extends Vue { bus.$emit('add-tag', tag);
@Prop() readonly state!: MetadataLoadState;
@Prop() readonly metadata!: Metadata | null;
// for use in v-if
private readonly MetadataLoadState = MetadataLoadState;
tags: string[] = [];
created() {
bus.$on("change-tag", (tags: string[]) => this.tags = tags);
bus.$emit("resend-tag");
}
addTag(tag: string) {
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,
};
}
} }
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.link-card-mini { .link-card-mini {
$height: 150px; $height: 150px;
overflow: hidden; overflow: hidden;
.row > div:first-child { .row > div:first-child {
display: flex; display: flex;
&:not([display=none]) { &:not([display='none']) {
min-height: $height; min-height: $height;
img { img {
position: absolute; position: absolute;
}
} }
} }
.card-text {
white-space: pre-line;
}
} }
.metadata-tag-item { .card-text {
cursor: pointer; white-space: pre-line;
user-select: none;
} }
}
.metadata-tag-item {
cursor: pointer;
user-select: none;
}
</style> </style>

View File

@ -1,96 +1,96 @@
<template> <template>
<div :class="containerClass" @click="$refs.input.focus()"> <div :class="containerClass" @click="$refs.input.focus()">
<input :name="name" type="hidden" :value="tagValue"> <input :name="name" type="hidden" :value="tagValue" />
<ul class="list-inline d-inline"> <ul class="list-inline d-inline">
<li v-for="(tag, i) in tags" <li
v-for="(tag, i) in tags"
class="list-inline-item badge badge-primary tag-item" class="list-inline-item badge badge-primary tag-item"
@click="removeTag(i)"><span class="oi oi-tag"></span> {{ tag }} | x</li> @click="removeTag(i)"
:key="tag"
>
<span class="oi oi-tag"></span> {{ tag }} | x
</li>
</ul> </ul>
<input :id="id" <input :id="id" ref="input" type="text" class="tag-input" v-model="buffer" @keydown="onKeyDown" />
ref="input"
type="text"
class="tag-input"
v-model="buffer"
@keydown="onKeyDown">
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import {Vue, Component, Prop, Watch} from "vue-property-decorator"; import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
import {bus} from "../checkin"; import { bus } from '../checkin';
@Component @Component
export default class TagInput extends Vue { export default class TagInput extends Vue {
@Prop(String) readonly id!: string; @Prop(String) readonly id!: string;
@Prop(String) readonly name!: string; @Prop(String) readonly name!: string;
@Prop(String) readonly value!: string; @Prop(String) readonly value!: string;
@Prop(Boolean) readonly isInvalid!: boolean; @Prop(Boolean) readonly isInvalid!: boolean;
tags: string[] = this.value.trim() !== "" ? this.value.trim().split(" ") : []; tags: string[] = this.value.trim() !== '' ? this.value.trim().split(' ') : [];
buffer: string = ""; buffer = '';
created() { created() {
bus.$on("add-tag", (tag: string) => this.tags.indexOf(tag) === -1 && this.tags.push(tag)); 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)); bus.$on('resend-tag', () => bus.$emit('change-tag', this.tags));
} }
onKeyDown(event: KeyboardEvent) { onKeyDown(event: KeyboardEvent) {
if (this.buffer.trim() !== "") { if (this.buffer.trim() !== '') {
switch (event.key) { switch (event.key) {
case 'Tab': case 'Tab':
case 'Enter': case 'Enter':
case ' ': case ' ':
if ((event as any).isComposing !== true) { if ((event as any).isComposing !== true) {
this.tags.push(this.buffer.trim()); this.tags.push(this.buffer.trim());
this.buffer = ""; this.buffer = '';
} }
event.preventDefault();
break;
case 'Unidentified':
// ()
if (event.srcElement && (event.srcElement as HTMLInputElement).value.slice(-1) == ' ') {
this.tags.push(this.buffer.trim());
this.buffer = '';
event.preventDefault(); event.preventDefault();
break; }
case 'Unidentified': break;
// ()
if (event.srcElement && (event.srcElement as HTMLInputElement).value.slice(-1) == ' ') {
this.tags.push(this.buffer.trim());
this.buffer = "";
event.preventDefault();
}
break;
}
} else if (event.key === "Enter") {
//
event.preventDefault();
} }
} } else if (event.key === 'Enter') {
//
removeTag(index: number) { event.preventDefault();
this.tags.splice(index, 1);
}
@Watch("tags")
onTagsChanged() {
bus.$emit("change-tag", this.tags);
}
get containerClass(): object {
return {
"form-control": true,
"h-auto": true,
"is-invalid": this.isInvalid
};
}
get tagValue(): string {
return this.tags.join(" ");
} }
} }
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.tag-item { .tag-item {
cursor: pointer; cursor: pointer;
} }
.tag-input { .tag-input {
border: 0; border: 0;
outline: 0; outline: 0;
} }
</style> </style>

View File

@ -1,38 +0,0 @@
import Chart from 'chart.js';
const graph = document.getElementById('global-count-graph');
const labels = JSON.parse(document.getElementById('global-count-labels').textContent);
const data = JSON.parse(document.getElementById('global-count-data').textContent);
new Chart(graph.getContext('2d'), {
type: 'bar',
data: {
labels,
datasets: [{
data,
backgroundColor: 'rgba(0, 0, 0, .1)',
borderColor: 'rgba(0, 0, 0, .25)',
borderWidth: 1
}]
},
options: {
maintainAspectRatio: false,
legend: {
display: false
},
elements: {
line: {}
},
scales: {
xAxes: [{
display: false
}],
yAxes: [{
display: false,
ticks: {
beginAtZero: true
}
}]
}
}
});

View File

@ -0,0 +1,47 @@
import * as Chart from 'chart.js';
const graph = document.getElementById('global-count-graph') as HTMLCanvasElement;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const labels = JSON.parse(document.getElementById('global-count-labels')!.textContent as string);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const data = JSON.parse(document.getElementById('global-count-data')!.textContent as string);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
new Chart(graph.getContext('2d')!, {
type: 'bar',
data: {
labels,
datasets: [
{
data,
backgroundColor: 'rgba(0, 0, 0, .1)',
borderColor: 'rgba(0, 0, 0, .25)',
borderWidth: 1,
},
],
},
options: {
maintainAspectRatio: false,
legend: {
display: false,
},
elements: {
line: {},
},
scales: {
xAxes: [
{
display: false,
},
],
yAxes: [
{
display: false,
ticks: {
beginAtZero: true,
},
},
],
},
},
});

View File

@ -1,5 +0,0 @@
$('#protected').on('change', function () {
if (!$(this).prop('checked')) {
alert('チェックイン履歴を公開に切り替えると、個別に非公開設定されているものを除いた全てのチェックインが誰でも閲覧できるようになります。\nご注意ください。');
}
});

View File

@ -0,0 +1,7 @@
$('#protected').on('change', function () {
if (!$(this).prop('checked')) {
alert(
'チェックイン履歴を公開に切り替えると、個別に非公開設定されているものを除いた全てのチェックインが誰でも閲覧できるようになります。\nご注意ください。'
);
}
});

View File

@ -1,72 +0,0 @@
(function ($) {
$.fn.linkCard = function (options) {
var settings = $.extend({
endpoint: '/api/checkin/card'
}, options);
return this.each(function () {
var $this = $(this);
$.ajax({
url: settings.endpoint,
method: 'get',
type: 'json',
data: {
url: $this.find('a').attr('href')
}
}).then(function (data) {
var $metaColumn = $this.find('.col-12:last-of-type');
var $imageColumn = $this.find('.col-12:first-of-type');
var $title = $this.find('.card-title');
var $desc = $this.find('.card-text');
var $image = $imageColumn.find('img');
if (data.title === '') {
$title.hide();
} else {
$title.text(data.title);
}
if (data.description === '') {
$desc.hide();
} 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 this.on('change', function () {
location.href = $(this).find(':selected').data('href');
});
};
$.fn.deleteCheckinModal = function () {
return this.each(function () {
$(this).on('show.bs.modal', function (event) {
var target = $(event.relatedTarget);
var 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) {
var modal = $('#deleteCheckinModal');
var form = modal.find('form');
form.attr('action', form.attr('action').replace('@', modal.data('id')));
form.submit();
})
});
};
})(jQuery);

View File

@ -0,0 +1,77 @@
(function ($) {
$.fn.linkCard = function (options) {
const settings = $.extend(
{
endpoint: '/api/checkin/card',
},
options
);
return this.each(function () {
const $this = $(this);
$.ajax({
url: settings.endpoint,
method: 'get',
type: 'json',
data: {
url: $this.find('a').attr('href'),
},
}).then(function (data) {
const $metaColumn = $this.find('.col-12:last-of-type');
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 === '') {
$title.hide();
} else {
$title.text(data.title);
}
if (data.description === '') {
$desc.hide();
} 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 this.on('change', function () {
location.href = $(this).find(':selected').data('href');
});
};
$.fn.deleteCheckinModal = function () {
return this.each(function () {
$(this)
.on('show.bs.modal', function (event) {
// 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);

View File

@ -1,4 +1,5 @@
import CalHeatMap from 'cal-heatmap'; import * as CalHeatMap from 'cal-heatmap';
import { subMonths } from 'date-fns';
if (document.getElementById('cal-heatmap')) { if (document.getElementById('cal-heatmap')) {
new CalHeatMap().init({ new CalHeatMap().init({
@ -7,9 +8,10 @@ if (document.getElementById('cal-heatmap')) {
subDomain: 'day', subDomain: 'day',
domainLabelFormat: '%Y/%m', domainLabelFormat: '%Y/%m',
weekStartOnMonday: false, weekStartOnMonday: false,
start: new Date().setMonth(new Date().getMonth() - 9), start: subMonths(new Date(), 9),
range: 10, range: 10,
data: JSON.parse(document.getElementById('count-by-day').textContent), // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
legend: [1, 2, 3, 4] data: JSON.parse(document.getElementById('count-by-day')!.textContent as string),
legend: [1, 2, 3, 4],
}); });
} }

View File

@ -1,124 +0,0 @@
import CalHeatMap from 'cal-heatmap';
import Chart from 'chart.js';
import {addMonths, format} from 'date-fns';
const graphData = JSON.parse(document.getElementById('graph-data').textContent);
function createLineGraph(id, labels, data) {
const context = document.getElementById(id).getContext('2d');
return new Chart(context, {
type: 'line',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1
}]
},
options: {
legend: {
display: false
},
elements: {
line: {
tension: 0
}
},
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
},
tooltips: {
mode: 'index',
intersect: false,
}
}
});
}
function createBarGraph(id, labels, data) {
const context = document.getElementById(id).getContext('2d');
new Chart(context, {
type: 'bar',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1
}]
},
options: {
legend: {
display: false
},
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
},
tooltips: {
mode: 'index',
intersect: false,
}
}
});
}
/**
* @param {Date} from
*/
function createMonthlyGraphData(from) {
const keys = [];
const values = [];
for (let i = 0; i < 12; i++) {
const current = addMonths(from, i);
const yearAndMonth = format(current, 'YYYY/MM');
keys.push(yearAndMonth);
values.push(graphData.monthlySum[yearAndMonth] || 0);
}
return {keys, values};
}
function getCurrentYear() {
const year = location.pathname.split('/').pop();
return /^(20[0-9]{2})$/.test(year) ? year : null;
}
if (document.getElementById('cal-heatmap')) {
new CalHeatMap().init({
itemSelector: '#cal-heatmap',
domain: 'month',
subDomain: 'day',
domainLabelFormat: '%Y/%m',
weekStartOnMonday: false,
start: new Date(getCurrentYear(), 0, 1, 0, 0, 0, 0),
range: 12,
data: graphData.dailySum,
legend: [1, 2, 3, 4]
});
}
if (document.getElementById('monthly-graph')) {
const {keys: monthlyKey, values: monthlySum} = createMonthlyGraphData(new Date(getCurrentYear(), 0, 1, 0, 0, 0, 0));
createLineGraph('monthly-graph', monthlyKey, monthlySum);
}
if (document.getElementById('yearly-graph')) {
createLineGraph('yearly-graph', graphData.yearlyKey, graphData.yearlySum);
}
if (document.getElementById('hourly-graph')) {
createBarGraph('hourly-graph', graphData.hourlyKey, graphData.hourlySum);
}
if (document.getElementById('dow-graph')) {
createBarGraph('dow-graph', ['日', '月', '火', '水', '木', '金', '土'], graphData.dowSum);
}

View File

@ -0,0 +1,138 @@
import * as CalHeatMap from 'cal-heatmap';
import * as Chart from 'chart.js';
import { addMonths, format } from 'date-fns';
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const graphData = JSON.parse(document.getElementById('graph-data')!.textContent as string);
function createLineGraph(id: string, labels: string[], data: any) {
const context = (document.getElementById(id) as HTMLCanvasElement).getContext('2d');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return new Chart(context!, {
type: 'line',
data: {
labels: labels,
datasets: [
{
data: data,
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1,
},
],
},
options: {
legend: {
display: false,
},
elements: {
line: {
tension: 0,
},
},
scales: {
yAxes: [
{
ticks: {
beginAtZero: true,
},
},
],
},
tooltips: {
mode: 'index',
intersect: false,
},
},
});
}
function createBarGraph(id: string, labels: string[], data: any) {
const context = (document.getElementById(id) as HTMLCanvasElement).getContext('2d');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
new Chart(context!, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
data: data,
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 1,
},
],
},
options: {
legend: {
display: false,
},
scales: {
yAxes: [
{
ticks: {
beginAtZero: true,
},
},
],
},
tooltips: {
mode: 'index',
intersect: false,
},
},
});
}
function createMonthlyGraphData(from: Date) {
const keys = [];
const values = [];
for (let i = 0; i < 12; i++) {
const current = addMonths(from, i);
const yearAndMonth = format(current, 'YYYY/MM');
keys.push(yearAndMonth);
values.push(graphData.monthlySum[yearAndMonth] || 0);
}
return { keys, values };
}
function getCurrentYear(): number {
const year = location.pathname.split('/').pop() || '';
if (/^(20[0-9]{2})$/.test(year)) {
return parseInt(year, 10);
} else {
throw 'Invalid year';
}
}
if (document.getElementById('cal-heatmap')) {
new CalHeatMap().init({
itemSelector: '#cal-heatmap',
domain: 'month',
subDomain: 'day',
domainLabelFormat: '%Y/%m',
weekStartOnMonday: false,
start: new Date(getCurrentYear(), 0, 1, 0, 0, 0, 0),
range: 12,
data: graphData.dailySum,
legend: [1, 2, 3, 4],
});
}
if (document.getElementById('monthly-graph')) {
const { keys: monthlyKey, values: monthlySum } = createMonthlyGraphData(
new Date(getCurrentYear(), 0, 1, 0, 0, 0, 0)
);
createLineGraph('monthly-graph', monthlyKey, monthlySum);
}
if (document.getElementById('yearly-graph')) {
createLineGraph('yearly-graph', graphData.yearlyKey, graphData.yearlySum);
}
if (document.getElementById('hourly-graph')) {
createBarGraph('hourly-graph', graphData.hourlyKey, graphData.hourlySum);
}
if (document.getElementById('dow-graph')) {
createBarGraph('dow-graph', ['日', '月', '火', '水', '木', '金', '土'], graphData.dowSum);
}

View File

@ -1,4 +0,0 @@
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}

View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'ログイン情報が一致しません。',
'throttle' => 'ログイン試行が多すぎます。 :seconds 秒後にやりなおしてください。',
];

View File

@ -12,22 +12,27 @@ require('laravel-mix-bundle-analyzer')
| |
*/ */
mix.js('resources/assets/js/app.js', 'public/js') mix.ts('resources/assets/js/app.ts', 'public/js')
.js('resources/assets/js/home.js', 'public/js') .ts('resources/assets/js/home.ts', 'public/js')
.js('resources/assets/js/user/profile.js', 'public/js/user') .ts('resources/assets/js/user/profile.ts', 'public/js/user')
.js('resources/assets/js/user/stats.js', 'public/js/user') .ts('resources/assets/js/user/stats.ts', 'public/js/user')
.js('resources/assets/js/setting/privacy.js', 'public/js/setting') .ts('resources/assets/js/setting/privacy.ts', 'public/js/setting')
.js('resources/assets/js/setting/import.js', 'public/js/setting') .ts('resources/assets/js/setting/import.ts', 'public/js/setting')
.js('resources/assets/js/setting/deactivate.js', 'public/js/setting') .ts('resources/assets/js/setting/deactivate.ts', 'public/js/setting')
.ts('resources/assets/js/checkin.ts', 'public/js') .ts('resources/assets/js/checkin.ts', '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']
}) })
.extract(['jquery', 'bootstrap']) .extract(['jquery', 'bootstrap'])
.extract(['chart.js', 'chartjs-color', 'color-name', 'moment'], 'public/js/vendor/chart') .extract(['chart.js', 'chartjs-color', 'color-name', 'moment', 'cal-heatmap', 'd3'], 'public/js/vendor/chart')
.version(); .version()
.webpackConfig(webpack => ({
externals: {
moment: 'moment'
}
}));
if (process.argv.includes('-a')) { if (process.argv.includes('-a')) {
mix.bundleAnalyzer({analyzerMode: 'static'}); mix.bundleAnalyzer({ analyzerMode: 'static' });
} }

995
yarn.lock

File diff suppressed because it is too large Load Diff