Contest language & top buttons
All checks were successful
CI / scan_ruby (push) Successful in 18s
CI / scan_js (push) Successful in 13s
CI / lint (push) Successful in 13s
CI / test (push) Successful in 33s

This commit is contained in:
sto 2025-06-20 08:07:39 +02:00
parent 71f2bb6b70
commit ac3b354480
13 changed files with 83 additions and 49 deletions

View File

@ -70,6 +70,8 @@ class ContestsController < ApplicationController
end
authorize @contest
I18n.locale = @contest.lang
@title = I18n.t("contests.scoreboard.title", name: @contest.name)
@contestants = @contest.contestants.sort_by { |contestant| [ -contestant.completions.size, contestant.time_seconds ] }
@puzzles = @contest.puzzles.order(:id)
@ -89,6 +91,6 @@ class ContestsController < ApplicationController
end
def contest_params
params.expect(contest: [ :name, :team, :allow_registration ])
params.expect(contest: [ :lang, :name, :team, :allow_registration ])
end
end

View File

@ -1,3 +1,3 @@
module Languages
AVAILABLE_LANGUAGES = [ { id: "en", name: "English" }, { id: "fr", name: "French" } ]
AVAILABLE_LANGUAGES = [ { id: "en", name: "English" }, { id: "fr", name: "Français" } ]
end

View File

@ -4,6 +4,7 @@
#
# id :integer not null, primary key
# allow_registration :boolean default(FALSE)
# lang :string default("en")
# name :string
# slug :string
# team :boolean default(FALSE)
@ -32,6 +33,7 @@ class Contest < ApplicationRecord
friendly_id :name, use: :slugged
validates :name, presence: true
validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } }
generates_token_for :token
end

View File

@ -4,6 +4,11 @@
.form-floating
= form.text_field :name, autocomplete: "off", class: "form-control"
= form.label :name, class: "required"
.row.mb-3
.col
.form-floating
= form.select :lang, Languages::AVAILABLE_LANGUAGES.map { |lang| [ lang[:name], lang[:id] ] }, {}, class: "form-select"
= form.label :lang
.row.mb-3
.col
.form-check.form-switch

View File

@ -2,13 +2,14 @@ table.table.table-striped.table-hover
thead
tr
th scope="col"
| Rank
= t("helpers.rank")
th scope="col"
| Name
= t("activerecord.attributes.contestant.name")
- if @contest.puzzles.size > 1
th scope="col"
= t("activerecord.attributes.contestant.completions")
th scope="col"
| Completed puzzles
th scope="col"
| Total time
= t("activerecord.attributes.contestant.display_time")
tbody
- @contestants.each_with_index do |contestant, index|
tr scope="row"
@ -16,7 +17,8 @@ table.table.table-striped.table-hover
= index + 1
td
= contestant.name
td
= contestant.completions.length
- if @contest.puzzles.size > 1
td
= contestant.completions.length
td
= contestant.display_time

View File

@ -1,21 +1,31 @@
.row.mb-4
- if @badges.size > 0
.row.mb-4
.col
.badges style="margin-top: -18px; position: absolute"
- @badges.each do |badge|
span.badge.text-bg-info.me-2
= badge
javascript:
async function copyExtensionUrlToClipboard() {
await navigator.clipboard.writeText("#{message_url}?token=#{@contest.generate_token_for(:token)}");
}
.row.mb-5
.col
css:
.badges { margin-top: -18px; position: absolute; }
.badges
- @badges.each do |badge|
span.badge.text-bg-info.me-2
= badge
.row
.col.alert.alert-success
= t("contests.show.public_scoreboard")
= link_to root_url + "public/#{@contest.slug}", root_url + "public/#{@contest.slug}"
.row.mb-4
.col.alert.alert-success
|> URL for the public scoreboard extension:
= link_to "#{message_url}?token=#{@contest.generate_token_for(:token)}"
a.btn.btn-success href="/public/#{@contest.slug}"
= t("contests.show.open_public_scoreboard")
button.btn.btn-success.ms-3 onclick="copyExtensionUrlToClipboard()"
css:
button > svg {
margin-right: 2px;
margin-top: -3px;
}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16">
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z"/>
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z"/>
</svg>
=< t("contests.show.copy_extension_url")
.row.mb-4
.col-7

View File

@ -45,17 +45,18 @@ en:
display_relative_time: Time for this puzzle
puzzle: Puzzle
contest:
name: "Name"
team: "Team contest"
team_description: "For UI display purposes mainly"
allow_registration: "Allow registration"
allow_registration_description: "Generates a shareable registration form for this contest"
lang: Language for the public scoreboard
name: Name
team: Team contest
team_description: For UI display purposes mainly
allow_registration: Allow registration
allow_registration_description: Generates a shareable registration form for this contest
contestant:
completions: completions
display_time: Time
email: "Email"
name: "Name"
email_description: "Optional. Used for sending emails through this app, or for identifying participants whose gmeet handle doesn't match their registered name."
email: Email
name: Name
email_description: Optional. Used for sending emails through this app, or for identifying participants whose gmeet handle doesn't match their registered name.
csv_import:
file: File
separator: Separator
@ -128,9 +129,10 @@ en:
title: "%{name}"
show:
title: "%{name}"
add_participant: "Add contestant"
add_puzzle: "Add puzzle"
public_scoreboard: "Public scoreboard: "
add_participant: Add contestant
add_puzzle: Add puzzle
copy_extension_url: Copy the URL for connecting from the browser extension
open_public_scoreboard: Open public scoreboard
contestants:
convert_csv:
title: "Import participants"

View File

@ -16,17 +16,18 @@ fr:
display_relative_time: Temps pour ce puzzle
puzzle: Puzzle
contest:
name: "Nom"
team: "Concours par équipes"
team_description: "Principalement pour des raisons d'affichage"
allow_registration: "Autoriser l'inscription via l'interface"
allow_registration_description: "Génère un formulaire d'inscription pour ce concours"
lang: Langue pour le classement public
name: Nom
team: Concours par équipes
team_description: Principalement pour des raisons d'affichage
allow_registration: Autoriser l'inscription via l'interface
allow_registration_description: Génère un formulaire d'inscription pour ce concours
contestant:
completions: Complétions
display_time: Temps
email: "Email"
name: "Nom"
email_description: "Optionnel. Utile pour envoyer des emails aux participant.e.s depuis cette app, ou pour reconnaître les pseudos gmeet quand ils ne correspondent pas au nom préalablement entré."
email: Email
name: Nom
email_description: Optionnel. Utile pour envoyer des emails aux participant.e.s depuis cette app, ou pour reconnaître les pseudos gmeet quand ils ne correspondent pas au nom préalablement entré.
csv_import:
file: Fichier
separator: Délimiteur
@ -99,9 +100,10 @@ fr:
title: "%{name}"
show:
title: "%{name}"
add_participant: "Ajouter un.e participant.e"
add_puzzle: "Ajouter un puzzle"
public_scoreboard: "Classement public : "
add_participant: Ajouter un.e participant.e
add_puzzle: Ajouter un puzzle
copy_extension_url: Copier l'URL pour la connexion depuis l'extension web
open_public_scoreboard: Ouvrir le classement public
contestants:
convert_csv:
title: "Importer des participant.e.s"

View File

@ -0,0 +1,5 @@
class AddLangToContest < ActiveRecord::Migration[8.0]
def change
add_column :contests, :lang, :string, default: 'en'
end
end

3
db/schema.rb generated
View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_06_18_155041) do
ActiveRecord::Schema[8.0].define(version: 2025_06_20_051905) do
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
@ -74,6 +74,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_18_155041) do
t.boolean "team", default: false
t.boolean "allow_registration", default: false
t.string "slug"
t.string "lang", default: "en"
t.index ["slug"], name: "index_contests_on_slug", unique: true
t.index ["user_id"], name: "index_contests_on_user_id"
end

View File

@ -4,6 +4,7 @@
#
# id :integer not null, primary key
# allow_registration :boolean default(FALSE)
# lang :string default("en")
# name :string
# slug :string
# team :boolean default(FALSE)

View File

@ -6,6 +6,7 @@
#
# id :integer not null, primary key
# allow_registration :boolean default(FALSE)
# lang :string default("en")
# name :string
# slug :string
# team :boolean default(FALSE)

View File

@ -4,6 +4,7 @@
#
# id :integer not null, primary key
# allow_registration :boolean default(FALSE)
# lang :string default("en")
# name :string
# slug :string
# team :boolean default(FALSE)