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

View File

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

View File

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

View File

@ -4,6 +4,11 @@
.form-floating .form-floating
= form.text_field :name, autocomplete: "off", class: "form-control" = form.text_field :name, autocomplete: "off", class: "form-control"
= form.label :name, class: "required" = 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 .row.mb-3
.col .col
.form-check.form-switch .form-check.form-switch

View File

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

View File

@ -1,21 +1,31 @@
.row.mb-4 - if @badges.size > 0
.row.mb-4
.col .col
css: .badges style="margin-top: -18px; position: absolute"
.badges { margin-top: -18px; position: absolute; }
.badges
- @badges.each do |badge| - @badges.each do |badge|
span.badge.text-bg-info.me-2 span.badge.text-bg-info.me-2
= badge = badge
.row javascript:
.col.alert.alert-success async function copyExtensionUrlToClipboard() {
= t("contests.show.public_scoreboard") await navigator.clipboard.writeText("#{message_url}?token=#{@contest.generate_token_for(:token)}");
= link_to root_url + "public/#{@contest.slug}", root_url + "public/#{@contest.slug}" }
.row.mb-4 .row.mb-5
.col.alert.alert-success .col
|> URL for the public scoreboard extension: a.btn.btn-success href="/public/#{@contest.slug}"
= link_to "#{message_url}?token=#{@contest.generate_token_for(:token)}" = 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 .row.mb-4
.col-7 .col-7

View File

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

View File

@ -16,17 +16,18 @@ fr:
display_relative_time: Temps pour ce puzzle display_relative_time: Temps pour ce puzzle
puzzle: Puzzle puzzle: Puzzle
contest: contest:
name: "Nom" lang: Langue pour le classement public
team: "Concours par équipes" name: Nom
team_description: "Principalement pour des raisons d'affichage" team: Concours par équipes
allow_registration: "Autoriser l'inscription via l'interface" team_description: Principalement pour des raisons d'affichage
allow_registration_description: "Génère un formulaire d'inscription pour ce concours" allow_registration: Autoriser l'inscription via l'interface
allow_registration_description: Génère un formulaire d'inscription pour ce concours
contestant: contestant:
completions: Complétions completions: Complétions
display_time: Temps display_time: Temps
email: "Email" email: Email
name: "Nom" 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_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: csv_import:
file: Fichier file: Fichier
separator: Délimiteur separator: Délimiteur
@ -99,9 +100,10 @@ fr:
title: "%{name}" title: "%{name}"
show: show:
title: "%{name}" title: "%{name}"
add_participant: "Ajouter un.e participant.e" add_participant: Ajouter un.e participant.e
add_puzzle: "Ajouter un puzzle" add_puzzle: Ajouter un puzzle
public_scoreboard: "Classement public : " copy_extension_url: Copier l'URL pour la connexion depuis l'extension web
open_public_scoreboard: Ouvrir le classement public
contestants: contestants:
convert_csv: convert_csv:
title: "Importer des participant.e.s" 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. # 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| create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", 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 "team", default: false
t.boolean "allow_registration", default: false t.boolean "allow_registration", default: false
t.string "slug" t.string "slug"
t.string "lang", default: "en"
t.index ["slug"], name: "index_contests_on_slug", unique: true t.index ["slug"], name: "index_contests_on_slug", unique: true
t.index ["user_id"], name: "index_contests_on_user_id" t.index ["user_id"], name: "index_contests_on_user_id"
end end

View File

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

View File

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

View File

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