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: '
No games found
',
+ pending: '',
+ suggestion: Handlebars.compile(`
+
+ {{name}}
+ {{categories.length}} categories
+
+ `)
+ }
+ })
+}
+
+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==