diff --git a/.eslintrc.js b/.eslintrc.js index 315b9d6..9c54f9b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,10 +19,12 @@ module.exports = { parser: '@typescript-eslint/parser', sourceType: 'module', }, - plugins: ['prettier', 'vue', '@typescript-eslint'], + plugins: ['prettier', 'vue', '@typescript-eslint', 'jquery'], rules: { '@typescript-eslint/explicit-module-boundary-types': 0, '@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'jquery/no-ajax': 2, + 'jquery/no-ajax-events': 2, }, }; diff --git a/package.json b/package.json index 9f6056f..ccaf01b 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@types/chart.js": "^2.9.22", "@types/jquery": "^3.3.38", "@types/js-cookie": "^2.2.0", + "@types/qs": "^6.9.4", "@typescript-eslint/eslint-plugin": "^3.1.0", "@typescript-eslint/parser": "^3.1.0", "bootstrap": "^4.5.0", @@ -26,6 +27,7 @@ "date-fns": "^1.30.1", "eslint": "^7.2.0", "eslint-config-prettier": "^6.11.0", + "eslint-plugin-jquery": "^1.5.1", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-vue": "^6.2.2", "husky": "^1.3.1", @@ -37,6 +39,7 @@ "open-iconic": "^1.1.1", "popper.js": "^1.14.7", "prettier": "^2.0.5", + "qs": "^6.9.4", "resolve-url-loader": "^3.1.1", "sass": "^1.26.8", "sass-loader": "^7.1.0", @@ -62,7 +65,7 @@ "stylelint --fix", "git add" ], - "*.{ts,js,vue}" : [ + "*.{ts,js,vue}": [ "eslint --fix", "git add" ], diff --git a/resources/assets/js/app.ts b/resources/assets/js/app.ts index 7267f57..1196678 100644 --- a/resources/assets/js/app.ts +++ b/resources/assets/js/app.ts @@ -1,5 +1,5 @@ import * as Cookies from 'js-cookie'; -import jqXHR = JQuery.jqXHR; +import { fetchPostJson, fetchDeleteJson, ResponseError } from './fetch'; require('./bootstrap'); @@ -41,57 +41,46 @@ $(() => { const isLiked = $this.data('liked'); if (isLiked) { - const callback = (data: any) => { - $this.data('liked', false); - $this.find('.oi-heart').removeClass('text-danger'); - - const count = data.ejaculation ? data.ejaculation.likes_count : 0; - $this.find('.like-count').text(count ? count : ''); - }; - - $.ajax({ - url: '/api/likes/' + encodeURIComponent(targetId), - method: 'delete', - type: 'json', - }) - .then(callback) - .catch(function (xhr: jqXHR) { - if (xhr.status === 404) { - callback(JSON.parse(xhr.responseText)); - return; + fetchDeleteJson(`/api/likes/${encodeURIComponent(targetId)}`) + .then((response) => { + if (response.status === 200 || response.status === 404) { + return response.json(); } + throw new ResponseError(response); + }) + .then((data) => { + $this.data('liked', false); + $this.find('.oi-heart').removeClass('text-danger'); - console.error(xhr); + const count = data.ejaculation ? data.ejaculation.likes_count : 0; + $this.find('.like-count').text(count ? count : ''); + }) + .catch((e) => { + console.error(e); alert('いいねを解除できませんでした。'); }); } else { - const callback = (data: any) => { - $this.data('liked', true); - $this.find('.oi-heart').addClass('text-danger'); + fetchPostJson('/api/likes', { id: targetId }) + .then((response) => { + if (response.status === 200 || response.status === 409) { + return response.json(); + } + throw new ResponseError(response); + }) + .then((data) => { + $this.data('liked', true); + $this.find('.oi-heart').addClass('text-danger'); - const count = data.ejaculation ? data.ejaculation.likes_count : 0; - $this.find('.like-count').text(count ? count : ''); - }; - - $.ajax({ - url: '/api/likes', - method: 'post', - type: 'json', - data: { - id: targetId, - }, - }) - .then(callback) - .catch(function (xhr: jqXHR) { - if (xhr.status === 409) { - callback(JSON.parse(xhr.responseText)); - return; - } else if (xhr.status === 401) { + const count = data.ejaculation ? data.ejaculation.likes_count : 0; + $this.find('.like-count').text(count ? count : ''); + }) + .catch((e) => { + if (e instanceof ResponseError && e.response.status === 401) { alert('いいねするためにはログインしてください。'); return; } - console.error(xhr); + console.error(e); alert('いいねできませんでした。'); }); } diff --git a/resources/assets/js/bootstrap.ts b/resources/assets/js/bootstrap.ts index 99d9e9b..fbfc914 100644 --- a/resources/assets/js/bootstrap.ts +++ b/resources/assets/js/bootstrap.ts @@ -1,17 +1,5 @@ // jQuery import './tissue'; -// Setup global request header -const token = document.head.querySelector('meta[name="csrf-token"]'); -if (!token) { - console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); -} else { - $.ajaxSetup({ - headers: { - 'X-CSRF-TOKEN': token.content, - }, - }); -} - // Bootstrap import 'bootstrap'; diff --git a/resources/assets/js/checkin.ts b/resources/assets/js/checkin.ts index 7902965..4a1bf50 100644 --- a/resources/assets/js/checkin.ts +++ b/resources/assets/js/checkin.ts @@ -1,6 +1,7 @@ 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' }); @@ -47,19 +48,18 @@ new Vue({ fetchMetadata(url: string) { this.metadataLoadState = MetadataLoadState.Loading; - $.ajax({ - url: '/api/checkin/card', - method: 'get', - type: 'json', - data: { - url, - }, - }) + 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((_e) => { + .catch(() => { this.metadata = null; this.metadataLoadState = MetadataLoadState.Failed; }); diff --git a/resources/assets/js/fetch.ts b/resources/assets/js/fetch.ts new file mode 100644 index 0000000..01e1431 --- /dev/null +++ b/resources/assets/js/fetch.ts @@ -0,0 +1,71 @@ +import { stringify } from 'qs'; + +const token = document.head.querySelector('meta[name="csrf-token"]'); +if (!token) { + console.error('CSRF token not found'); +} + +const headers = { + 'X-CSRF-TOKEN': token?.content ?? '', +}; + +type QueryParams = { [key: string]: string }; + +const joinParamsToPath = (path: string, params: QueryParams) => + Object.keys(params).length === 0 ? path : `${path}?${stringify(params)}`; + +const fetchWrapper = (path: string, options: RequestInit = {}) => + fetch(path, { + credentials: 'same-origin', + ...options, + headers: { ...headers, ...options.headers }, + }); + +const fetchWithJson = (path: string, body?: any, options: RequestInit = {}) => + fetchWrapper(path, { + ...options, + body: body && JSON.stringify(body), + headers: { 'Content-Type': 'application/json', ...options.headers }, + }); + +const fetchWithForm = (path: string, body?: any, options: RequestInit = {}) => + fetchWrapper(path, { + ...options, + body: body && stringify(body), + headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...options.headers }, + }); + +export const fetchGet = (path: string, params: QueryParams = {}, options: RequestInit = {}) => + fetchWrapper(joinParamsToPath(path, params), { method: 'GET', ...options }); + +export const fetchPostJson = (path: string, body?: any, options: RequestInit = {}) => + fetchWithJson(path, body, { method: 'POST', ...options }); + +export const fetchPostForm = (path: string, body?: any, options: RequestInit = {}) => + fetchWithForm(path, body, { method: 'POST', ...options }); + +export const fetchPutJson = (path: string, body?: any, options: RequestInit = {}) => + fetchWithJson(path, body, { method: 'PUT', ...options }); + +export const fetchPutForm = (path: string, body?: any, options: RequestInit = {}) => + fetchWithForm(path, body, { method: 'PUT', ...options }); + +export const fetchDeleteJson = (path: string, body?: any, options: RequestInit = {}) => + fetchWithJson(path, body, { method: 'DELETE', ...options }); + +export const fetchDeleteForm = (path: string, body?: any, options: RequestInit = {}) => + fetchWithForm(path, body, { method: 'DELETE', ...options }); + +export class ResponseError extends Error { + response: Response; + + constructor(response: Response, ...rest: any) { + super(...rest); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ResponseError); + } + + this.name = 'ResponseError'; + this.response = response; + } +} diff --git a/resources/assets/js/tissue.ts b/resources/assets/js/tissue.ts index 1b7fe32..e059c75 100644 --- a/resources/assets/js/tissue.ts +++ b/resources/assets/js/tissue.ts @@ -1,3 +1,5 @@ +import { fetchGet } from './fetch'; + (function ($) { $.fn.linkCard = function (options) { const settings = $.extend( @@ -9,43 +11,44 @@ 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); - } + const url = $this.find('a').attr('href'); + if (!url) { + return; + } - if (data.description === '') { - $desc.hide(); - } else { - $desc.text(data.description); - } + fetchGet(settings.endpoint, { url }) + .then((response) => response.json()) + .then((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.image === '') { - $imageColumn.hide(); - $metaColumn.removeClass('col-md-6'); - } else { - $image.attr('src', data.image); - } + if (data.title === '') { + $title.hide(); + } else { + $title.text(data.title); + } - if (data.title !== '' || data.description !== '' || data.image !== '') { - $this.removeClass('d-none'); - } - }); + 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'); + } + }); }); }; diff --git a/yarn.lock b/yarn.lock index 96ceb48..34ff6b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -849,6 +849,11 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== +"@types/qs@^6.9.4": + version "6.9.4" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a" + integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== + "@types/sizzle@*": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" @@ -3030,6 +3035,11 @@ eslint-config-prettier@^6.11.0: dependencies: get-stdin "^6.0.0" +eslint-plugin-jquery@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jquery/-/eslint-plugin-jquery-1.5.1.tgz#d6bac643acf9484ce76394e27e2b07baca06662e" + integrity sha512-L7v1eaK5t80C0lvUXPFP9MKnBOqPSKhCOYyzy4LZ0+iK+TJwN8S9gAkzzP1AOhypRIwA88HF6phQ9C7jnOpW8w== + eslint-plugin-prettier@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz#168ab43154e2ea57db992a2cd097c828171f75c2" @@ -6684,6 +6694,11 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.9.4: + version "6.9.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"