diff --git a/app/assets/stylesheets/search.sass b/app/assets/stylesheets/search.sass index 3eb386eb6..702008de5 100644 --- a/app/assets/stylesheets/search.sass +++ b/app/assets/stylesheets/search.sass @@ -1,6 +1,6 @@ .twitter-typeahead, .tt-hint, .tt-input, .tt-menu - width: 75% + width: 100% -.twitter-typeahead, .tt-hint, .tt-input +.search, .tt-hint, .tt-input display: block !important diff --git a/app/controllers/games_controller.rb b/app/controllers/games_controller.rb index d9e4331b3..3efaf2d1e 100644 --- a/app/controllers/games_controller.rb +++ b/app/controllers/games_controller.rb @@ -19,18 +19,6 @@ def show def edit end - def update - if params[:sync_srdc] - @game.sync_with_srdc - end - - if params[:sync_srl] - @game.sync_with_srdc - end - - redirect_to edit_game_path(@game), notice: 'Done!' - end - private def authorize @@ -48,7 +36,7 @@ def set_game def set_games @games = Hash.new { |h, k| h[k] = [] } - SpeedrunDotComGame.order('ASCII(name) ASC, UPPER(name) ASC').each do |game| + Game.joins(:runs, :srdc).order('ASCII(games.name) ASC, UPPER(games.name) ASC').each do |game| @games[game.name[0].downcase] << game end end diff --git a/app/javascript/game_select.js b/app/javascript/game_select.js new file mode 100644 index 000000000..e7def8dff --- /dev/null +++ b/app/javascript/game_select.js @@ -0,0 +1,78 @@ +import typeahead from "typeahead.js"; +import Bloodhound from 'bloodhound-js' +import Handlebars from 'handlebars' + +document.addEventListener('turbolinks:before-cache', function() { + $('.game-select').typeahead('destroy') +}) + +const loadGameSelector = function() { + $('.game-select').typeahead({ + minLength: 3, + classNames: { + hint: 'text-muted', + menu: 'dropdown-menu', + selectable: 'dropdown-item', + cursor: 'active' + } + }, + { + name: 'games', + display: game => game.name, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.whitespace, + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/api/v4/games', + prepare: function(query, settings) { + return Object.assign(settings, {url: encodeURI(`${settings.url}?search=${query.trim()}`)}) + }, + transform: response => response.games.map(game => Object.assign(game, {'_type': 'game'})), + }, + identify: game => game.id, + }), + templates: { + notFound: '', + pending: '', + suggestion: Handlebars.compile(` + + `) + } + }) +} + +document.addEventListener('turbolinks:load', loadGameSelector) + +document.addEventListener('click', function(event) { + if (!event.target.closest('.change-selected-game')) { + return + } + + document.getElementById('selected-game-id').value = event.target.dataset['id'] + document.getElementById('category-selector').focus() + + const categorySelect = document.getElementById('category-selector') + const saveButton = document.getElementById('game-category-submit') + const loading = document.createElement('option') + loading.text = 'Loading...' + + Array.from(categorySelect.children).forEach((option) => option.remove()) + categorySelect.appendChild(loading) + + fetch(`/api/v4/games?search=${document.getElementById('selected-game-id').value}`).then(response => response.json()).then(response => { + Array.from(categorySelect.children).forEach((option) => option.remove()) + response.games[0].categories.forEach(category => { + const option = document.createElement('option') + option.value = category.id + option.text = category.name + categorySelect.appendChild(option) + }) + saveButton.disabled = false + categorySelect.disabled = false + }) +}) + +export { loadGameSelector } diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index b92026355..445b445fe 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -52,13 +52,13 @@ import "../count.js" import "../crisp.js" import "../highchart_theme.js" import "../chart_builder.js" +import "../game_select.js" import "../landing.js" import "../like.js" import "../race_attach.js" import "../run_claim.js" import '../run_delete.js' import '../run_disown.js' -import "../run_edit.js" import "../run_export.js" import "../run_parse.js" import "../search.js" diff --git a/app/javascript/run_edit.js b/app/javascript/run_edit.js deleted file mode 100644 index 44da0dfab..000000000 --- a/app/javascript/run_edit.js +++ /dev/null @@ -1,33 +0,0 @@ -document.addEventListener('turbolinks:load', function() { - const gameSelect = document.getElementById('game-selector') - const categorySelect = document.getElementById('category-selector') - const saveButton = document.getElementById('game-category-submit') - - const loading = document.createElement('option') - loading.text = 'Loading...' - - if(gameSelect === null) { - return - } - - gameSelect.addEventListener('change', function(event) { - saveButton.disabled = true - categorySelect.disabled = true - Array.from(categorySelect.children).forEach((option) => option.remove()) - categorySelect.appendChild(loading) - - fetch(`/api/v4/games/${this.value}`).then(function(response) { - return response.json() - }).then(function(response) { - Array.from(categorySelect.children).forEach((option) => option.remove()) - response.game.categories.forEach(function(category) { - const option = document.createElement('option') - option.value = category.id - option.text = category.name - categorySelect.appendChild(option) - }) - saveButton.disabled = false - categorySelect.disabled = false - }) - }) -}) diff --git a/app/javascript/vue/race-title.js b/app/javascript/vue/race-title.js index 62e65c01a..77430c3ac 100644 --- a/app/javascript/vue/race-title.js +++ b/app/javascript/vue/race-title.js @@ -1,9 +1,13 @@ import raceNav from './race-nav.js' import { getAccessToken } from '../token' +import { loadGameSelector } from '../game_select.js' +import VueBootstrapTypeahead from 'vue-bootstrap-typeahead' +const _ = require('underscore') export default { components: { raceNav, + VueBootstrapTypeahead, }, computed: { categories: function() { @@ -19,9 +23,6 @@ export default { category: function() { return this.categories.find(category => category.id === this.categoryId) }, - game: function() { - return this.games.find(game => game.id === this.gameId) - }, title: function() { if (this.race === null) { return '' @@ -34,7 +35,6 @@ export default { }, created: async function() { this.notes = this.race.notes - fetch('/api/v4/games').then(response => response.json()).then(body => this.games = body.games) this.gameId = (this.race.game || {id: null}).id this.categoryId = (this.race.category || {id: null}).id @@ -51,14 +51,22 @@ export default { return entry.runner.id === this.currentUser.id }) + + this.game = await fetch(`/api/v4/games?search=${this.race.game.id}`).then(response => response.json()).then(body => { + return body.games[0] + }) + + this.gameQuery = this.race.game.name }, data: () => ({ categoryId: null, editing: false, entry: null, error: null, + game: null, gameId: null, - games: [], + gameQuery: null, + gameResults: [], loading: false, notes: '', }), @@ -105,13 +113,20 @@ export default { } }, }, + mounted: function() { + }, name: 'race-title', props: ['race', 'starting', 'syncing'], watch: { gameId: function() { - if (this.game.categories.find(category => category.id === this.categoryId) === undefined) { + if (!this.game || this.game.categories.find(category => category.id === this.categoryId) === undefined) { this.categoryId = null } }, + gameQuery: _.debounce(function(newGame) { + fetch(`/api/v4/games?search=${newGame}`).then(response => response.json()).then(body => { + this.gameResults = body.games + }) + }, 500) }, } diff --git a/app/views/games/edit.slim b/app/views/games/edit.slim index 5f7a031d6..c7dbad910 100644 --- a/app/views/games/edit.slim +++ b/app/views/games/edit.slim @@ -22,13 +22,3 @@ li If a category has an identical name to a #{@game} category, the two will be merged into one #{@game} category. li You must have moderation priveleges for both games to merge. - .col.p-1 - .card.h-100 - .card-header Resync - .card-body - = button_to game_path(@game), method: :patch, params: {'sync_srdc' => 1}, class: 'btn btn-srdc m-2' - => image_tag(asset_path('srdc.png'), style: 'height: 0.8em') - ' Sync with Speedrun.com - = button_to game_path(@game), method: :patch, params: {'sync_srl' => 1}, class: 'btn btn-light m-2' - => image_tag(asset_path('srl.png'), style: 'height: 0.8em') - ' Sync with SpeedRunsLive diff --git a/app/views/games/index.slim b/app/views/games/index.slim index f2f163511..a51cc7f81 100644 --- a/app/views/games/index.slim +++ b/app/views/games/index.slim @@ -1,13 +1,16 @@ - content_for(:title, 'Games') .row: .col-md-12 + .alert.m-3 + ' Searching #{number_with_delimiter(@games.sum(&:count))} games with at least one run. + ' In total there are #{number_with_delimiter(Game.count)} games. input#search.form-control.mb-4 type='text' placeholder='Search games...' .card-columns.w-100 - - @games.each do |label, srdc_games| + - @games.each do |label, games| .card.game-section.w-100 .card-header = label .list-group.list-group-flush - - srdc_games.each do |srdc_game| + - games.each do |game| a.list-group-item.list-group-item-action.py-1.game.text-light( - href=game_path(srdc_game.shortname) - data={name: srdc_game.name} - ) = srdc_game.name + href=game_path(game.srdc&.shortname) + data={name: game.name} + ) = game.name diff --git a/app/views/layouts/application.slim b/app/views/layouts/application.slim index 0bdc9a821..3509ac92e 100644 --- a/app/views/layouts/application.slim +++ b/app/views/layouts/application.slim @@ -73,9 +73,6 @@ html = form_for(:search, method: :get, url: search_path, html: {class: 'form-inline ml-2 flex-grow-1', role: 'search'}) do |f| .input-group = f.text_field(:q, name: :q, class: 'form-control search text-dark', value: @query, placeholder: 'Search...') - .input-group-append - = button_tag(type: 'submit', class: 'btn btn-secondary', name: nil) do - = icon('fas', 'search') ul.nav.navbar-nav - if current_user.present? li.nav-item class=('active' if on_a_profile_page?) diff --git a/app/views/races/_title.slim b/app/views/races/_title.slim index b3fe196ca..bd07e5099 100644 --- a/app/views/races/_title.slim +++ b/app/views/races/_title.slim @@ -11,12 +11,19 @@ race-title( h1 v-if='!editing' = '{{title}}' = form_for race, url: race_path(race), html: {'v-if' => 'editing'} do |f| .form-row - .col-md-6.my-1 - label.sr-only for='game-selector' - select.form-control v-model='gameId' - option v-for='game in games' :value='game.id' {{game.name}} - .col-md-6.my-1 - label.sr-only for='category-selector' + = f.hidden_field(:game, id: 'selected-game-id', value: race.game&.id) + .my-3.col-md-12 + label for='game-selector' Game + vue-bootstrap-typeahead.mb-1( + background-variant='dark' + placeholder='Change game by typing...' + text-variant='light' + :data='gameResults' + @hit='game = $event' + :serializer='game => game.name' + v-model='gameQuery' + ) + label for='category-selector' Category select.form-control v-model='categoryId' option v-for='category in categories' :value='category.id' {{category.name}} .row diff --git a/app/views/runs/edit.slim b/app/views/runs/edit.slim index 94426db7c..b231dc602 100644 --- a/app/views/runs/edit.slim +++ b/app/views/runs/edit.slim @@ -22,20 +22,10 @@ h5.card-header Edit Game/Category .card-body .form-row + = f.hidden_field(:game, id: 'selected-game-id', value: @run.game&.id) .col-md-6.my-1 label.sr-only for='game-selector' - = f.collection_select( \ - :game, \ - SpeedrunDotComGame.order(:name), \ - :shortname, \ - :name, \ - { \ - selected: @run.game.try(:srdc).try(:shortname), \ - include_blank: true \ - }, \ - id: 'game-selector', \ - class: 'form-control' \ - ) + = f.text_field(:q, name: :q, class: 'form-control game-select text-dark', value: @run.game&.name, placeholder: 'Game...') .col-md-6.my-1 label.sr-only for='category-selector' = f.collection_select( \ @@ -44,7 +34,7 @@ :id, \ :name, \ { \ - selected: @run.category.try(:id) \ + selected: @run.category&.id, \ }, \ id: 'category-selector', \ class: 'form-control' \ diff --git a/app/views/search/index.slim b/app/views/search/index.slim index d91721f01..73f54f73c 100644 --- a/app/views/search/index.slim +++ b/app/views/search/index.slim @@ -11,9 +11,6 @@ .form-group .input-group = f.text_field(:q, name: :q, autofocus: @query.blank?, value: @query, class: 'form-control search bg-dark') - .input-group-append - = button_tag(type: 'submit', class: 'btn btn-primary', name: nil) do - = icon('fas', 'search') #search-results.card.mb-3.w-100 - @results.each do |search_type, results| diff --git a/package.json b/package.json index e1390b59c..597e56861 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "typeahead.js": "^0.11.1", "underscore": "^1.8.3", "vue": "^2.6.10", + "vue-bootstrap-typeahead": "^0.2.6", "vue-loader": "^15.7.0", "vue-multiselect": "^2.1.6", "vue-template-compiler": "^2.6.10", diff --git a/yarn.lock b/yarn.lock index be3add2cd..056aed9b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6248,6 +6248,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -7407,6 +7412,14 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vue-bootstrap-typeahead@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/vue-bootstrap-typeahead/-/vue-bootstrap-typeahead-0.2.6.tgz#8c1999a00bf4bf9fc906bae3a462482482cbc297" + integrity sha512-BcUAnvfN+PS0StL6E3endd37P7HUt9otk+8m7tsa2gkt2I2KY8O2Dma49oR8ie8iletvJAlAqpN+klF6ktPULQ== + dependencies: + resize-observer-polyfill "^1.5.0" + vue "^2.5.17" + vue-hot-reload-api@^2.3.0: version "2.3.4" resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2" @@ -7464,7 +7477,7 @@ vue-turbolinks@^2.0.4: dependencies: vue "^2.2.4" -vue@^2.2.4, vue@^2.6.10: +vue@^2.2.4, vue@^2.5.17, vue@^2.6.10: version "2.6.11" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==