Skip to content

Commit

Permalink
optimized MAL manga title lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaishiyoku committed Sep 13, 2020
1 parent 9b38491 commit c1cab5a
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 96 deletions.
13 changes: 8 additions & 5 deletions app/Http/Controllers/MangaController.php
Expand Up @@ -9,6 +9,7 @@
use App\Models\Volume;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Jikan\Exception\BadResponseException;
use Jikan\Exception\ParserException;
Expand Down Expand Up @@ -240,17 +241,19 @@ public function search(Request $request)
'query' => ['required', 'min:3'],
]);

$mangaSearchResults = Cache::remember('manga-search-' . sha1($data['query']), env('MANGA_SEARCH_CACHE_TTL_SECONDS', 60 * 60), function () use ($data) {
$query = Str::lower($data['query']);

$mangaSearchResults = Cache::remember('manga-search-' . sha1($query), env('MANGA_SEARCH_CACHE_TTL_SECONDS', 60 * 60), function () use ($query) {
$mangaSearchRequest = new MangaSearchRequest();
$mangaSearchRequest->setQuery($data['query']);
$mangaSearchRequest->setQuery($query);

$jikan = new MalClient();
return collect($jikan->getMangaSearch($mangaSearchRequest)->getResults())
->mapWithKeys(function (MangaSearchListItem $mangaSearchResult, $key) {
return [$key => [
->map(function (MangaSearchListItem $mangaSearchResult) {
return [
'malId' => $mangaSearchResult->getMalId(),
'title' => $mangaSearchResult->getTitle(),
]];
];
})
->sortBy('title')
->values();
Expand Down
13 changes: 11 additions & 2 deletions resources/css/app.css
Expand Up @@ -58,13 +58,17 @@
@apply border border-red-500;
}

.has-error-alternative {
@apply bg-red-100 text-red-800 border border-red-400;
}

/* Buttons
---------------------------------------------------------------------------- */
.btn-default {
@apply border border-purple-600 bg-purple-500 text-gray-100 py-2 px-4 rounded transition-all duration-200 shadow;
}
.btn-default:hover,
.btn-with-input:hover {
.btn-default:hover:not(:disabled),
.btn-with-input:hover:not(:disabled) {
@apply border-purple-800 bg-purple-700;
}
.btn-default:focus,
Expand All @@ -76,6 +80,11 @@
@apply bg-purple-500 text-gray-100 py-2 px-4 rounded-r transition-all duration-150 border-l border-purple-600;
}

.btn-default:disabled,
.btn-with-input:disabled {
@apply cursor-not-allowed border-purple-500 bg-purple-400;
}

/* Links
---------------------------------------------------------------------------- */
.link-default {
Expand Down
120 changes: 54 additions & 66 deletions resources/js/app.js
Expand Up @@ -37,6 +37,60 @@ onDomReady(() => {
});
});

document.querySelectorAll('[data-provide-manga-search]').forEach((element) => {
const url = element.getAttribute('data-url');
const searchInputElement = document.querySelector(element.getAttribute('data-manga-search-input'));
const targetInputElement = document.querySelector(element.getAttribute('data-target-input'));
const dropdownElement = document.querySelector(element.getAttribute('data-dropdown'));
const searchResultContainer = document.querySelector(element.getAttribute('data-manga-search-results-container'));
const buttonText = element.textContent;

searchInputElement.addEventListener('keyup', (event) => {
element.disabled = searchInputElement.value.length < 3;
});

element.addEventListener('click', (event) => {
searchInputElement.classList.remove('has-error-alternative');
searchResultContainer.innerHTML = '';

let loadingSpinnerElement = document.createElement('i');
loadingSpinnerElement.classList.add('fas', 'fa-spinner', 'fa-spin');

element.textContent = '';
element.appendChild(loadingSpinnerElement);
element.disabled = true;

axios.post(url, {query: searchInputElement.value})
.then(({data}) => {
data.forEach((item) => {
let resultElement = document.createElement('div');
resultElement.setAttribute('data-manga-search-result-id', item.malId);
resultElement.classList.add('dropdown-item');
resultElement.textContent = item.title;

searchResultContainer.appendChild(resultElement);

searchResultContainer.querySelectorAll('[data-manga-search-result-id]').forEach((el) => {
el.addEventListener('click', () => {
targetInputElement.value = el.getAttribute('data-manga-search-result-id');
searchInputElement.value = '';
searchResultContainer.innerHTML = '';
element.disabled = true;
});
});

element.innerHTML = buttonText;
element.disabled = false;
});
})
.catch(() => {
searchInputElement.classList.add('has-error-alternative');
element.innerHTML = buttonText;
element.disabled = false;
});
});
});

tippy('[data-tooltip-content]', {
theme: 'light-border',
content: (reference) => reference.getAttribute('data-tooltip-content'),
Expand All @@ -58,70 +112,4 @@ onDomReady(() => {
return dropdown;
},
});

document.querySelectorAll('[data-provide-typeahead]').forEach((element) => {
const targetElement = document.querySelector(element.getAttribute('data-target'));
const targetProperty = element.getAttribute('data-target-property');
const url = element.getAttribute('data-url');
const minLength = parseInt(element.getAttribute('data-min-length'), 10) || 0
const loadingIndicatorElement = document.querySelector(element.getAttribute('data-loading-indicator'));

const {load_more, loading} = window.config.typeahead;

bonanza(element, {
templates: {
itemLabel: (obj) => obj.title,
// item: '',
label: (obj) => obj.title,
isDisabled: (obj) => false,
noResults: (search) => `No results for ${search}`,
loadMore: load_more,
loading: loading,
},
css: {
container: 'dropdown absolute overflow-y-auto max-h-48', // div
hide: 'hidden',
list: '', // ul
item: 'dropdown-item', // li
disabled: '',
selected: 'dropdown-item-active',
// loading: '', // li
// loadMore: '', // li
// noResults: '', // li
// inputLoading: '', // input
match: '',
},
openOnFocus: true,
showLoading: true,
showLoadMore: true,
limit: 10,
scrollDistance: 0,
getItems: (result) => result,
}, (query, callback) => {
if (query.search.length >= minLength) {
axios.post(url, {query: query.search})
.then(({data}) => {
callback(null, data);
})
.catch(() => {
element.classList.remove('input-typeahead');
element.classList.add('input-default');
loadingIndicatorElement.classList.add('hidden');
});
}
})
.on('change', (item) => {
targetElement.value = item[targetProperty];
})
.on('search', (query) => {
element.classList.remove('input-default');
element.classList.add('input-typeahead');
loadingIndicatorElement.classList.remove('hidden');
})
.on('success', (result, query) => {
element.classList.remove('input-typeahead');
element.classList.add('input-default');
loadingIndicatorElement.classList.add('hidden');
});
});
});
1 change: 1 addition & 0 deletions resources/lang/de/common.php
Expand Up @@ -39,4 +39,5 @@
'loading' => 'Lade...',
],
],
'search' => 'Suchen',
];
1 change: 1 addition & 0 deletions resources/lang/de/manga.php
Expand Up @@ -43,4 +43,5 @@
'mal_item_error' => 'Beim Holen das Manga-Informationen von MyAnimeList ist ein Fehler aufgetreten.',
'volumes' => 'Band|Bände',
'specials' => 'Special|Specials',
'search_on_mal' => 'Auf MAL suchen',
];
2 changes: 1 addition & 1 deletion resources/lang/de/validation.php
Expand Up @@ -185,6 +185,6 @@
'current_password' => 'Derzeitiges Passwort',
'new_password' => 'Neues Passwort',
'new_password_confirmation' => 'Neues Passwort (Wiederholung)',
'mal_title' => 'MAL Manga',
'mal_title' => 'Titel',
],
];
1 change: 1 addition & 0 deletions resources/lang/en/common.php
Expand Up @@ -39,4 +39,5 @@
'loading' => 'Loading...',
],
],
'search' => 'Search',
];
1 change: 1 addition & 0 deletions resources/lang/en/manga.php
Expand Up @@ -43,4 +43,5 @@
'mal_item_error' => 'There was an error fetching the manga information from MyAnimeList.',
'volumes' => 'Volume|Volumes',
'specials' => 'Special|Specials',
'search_on_mal' => 'Search on MAL',
];
2 changes: 1 addition & 1 deletion resources/lang/en/validation.php
Expand Up @@ -186,6 +186,6 @@
'current_password' => 'Current password',
'new_password' => 'New password',
'new_password_confirmation' => 'New password confirmation',
'mal_title' => 'MAL Manga',
'mal_title' => 'Title',
],
];
46 changes: 25 additions & 21 deletions resources/views/manga/_form.blade.php
Expand Up @@ -11,27 +11,6 @@
@endif
</div>

<div class="mb-4">
{{ Form::label('mal_title', __('validation.attributes.mal_title'), ['class' => 'label-default']) }}

{{ Form::text('mal_title', optional($manga->malItem)->title_english, [
'data-provide-typeahead' => true,
'data-url' => route('mangas.search'),
'data-target' => 'input[name="mal_id"]',
'data-loading-indicator' => '#manga-search-loading-indicator',
'data-target-property' => 'malId',
'data-min-length' => 3,
'class' => 'input-default',
]) }}

<div id="manga-search-loading-indicator" class="absolute ml-3 hidden" style="margin-top: -1.8rem;">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-purple-900" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</div>

<div class="mb-4">
{{ Form::label('mal_id', __('validation.attributes.mal_id'), ['class' => 'label-default']) }}

Expand All @@ -42,6 +21,31 @@
{{ $errors->first('mal_id') }}
</p>
@endif

<button type="button" data-provide-dropdown data-dropdown-target="#manga-search-dropdown" class="link-default hover:underline mt-1">
{{ __('manga.search_on_mal') }}
</button>

<div id="manga-search-dropdown" class="dropdown hidden">
<div class="flex p-2">
<input type="text" id="manga-search-input" placeholder="{{ __('validation.attributes.mal_title') }}" class="flex-grow appearance-none rounded-l py-2 px-3 text-gray-700 bg-gray-100 leading-tight"/>
<button
type="button"
class="btn-with-input"
data-provide-manga-search
data-url="{{ route('mangas.search') }}"
data-target-input="#mal_id"
data-manga-search-input="#manga-search-input"
data-manga-search-results-container="#manga-search-results"
disabled
data-dropdown="#manga-search-dropdown"
>
{{ __('common.search') }}
</button>
</div>

<div id="manga-search-results" class="overflow-auto" style="max-height: 20rem;"></div>
</div>
</div>

<div class="mb-4">
Expand Down

0 comments on commit c1cab5a

Please sign in to comment.