From ac3b35448037b3ea3af3a1f39dde41b0f787c55f Mon Sep 17 00:00:00 2001 From: sto Date: Fri, 20 Jun 2025 08:07:39 +0200 Subject: [PATCH] Contest language & top buttons --- app/controllers/contests_controller.rb | 4 +- app/lib/languages.rb | 2 +- app/models/contest.rb | 2 + app/views/contests/_form.html.slim | 5 +++ app/views/contests/scoreboard.html.slim | 16 ++++--- app/views/contests/show.html.slim | 44 ++++++++++++------- config/locales/en.yml | 24 +++++----- config/locales/fr.yml | 24 +++++----- .../20250620051905_add_lang_to_contest.rb | 5 +++ db/schema.rb | 3 +- spec/factories/contests.rb | 1 + test/fixtures/contests.yml | 1 + test/models/contest_test.rb | 1 + 13 files changed, 83 insertions(+), 49 deletions(-) create mode 100644 db/migrate/20250620051905_add_lang_to_contest.rb diff --git a/app/controllers/contests_controller.rb b/app/controllers/contests_controller.rb index 6f51a3e..505f38b 100644 --- a/app/controllers/contests_controller.rb +++ b/app/controllers/contests_controller.rb @@ -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 diff --git a/app/lib/languages.rb b/app/lib/languages.rb index daddfa1..2c6ce1c 100644 --- a/app/lib/languages.rb +++ b/app/lib/languages.rb @@ -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 diff --git a/app/models/contest.rb b/app/models/contest.rb index de962e8..1b46a46 100644 --- a/app/models/contest.rb +++ b/app/models/contest.rb @@ -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 diff --git a/app/views/contests/_form.html.slim b/app/views/contests/_form.html.slim index 4717806..fad3110 100644 --- a/app/views/contests/_form.html.slim +++ b/app/views/contests/_form.html.slim @@ -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 diff --git a/app/views/contests/scoreboard.html.slim b/app/views/contests/scoreboard.html.slim index a45d418..7670007 100644 --- a/app/views/contests/scoreboard.html.slim +++ b/app/views/contests/scoreboard.html.slim @@ -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 \ No newline at end of file diff --git a/app/views/contests/show.html.slim b/app/views/contests/show.html.slim index e6a74ae..f8537f1 100644 --- a/app/views/contests/show.html.slim +++ b/app/views/contests/show.html.slim @@ -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; + } + + + + + =< t("contests.show.copy_extension_url") .row.mb-4 .col-7 diff --git a/config/locales/en.yml b/config/locales/en.yml index 9e801b1..6bb71b8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 7536089..7a37fd4 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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" diff --git a/db/migrate/20250620051905_add_lang_to_contest.rb b/db/migrate/20250620051905_add_lang_to_contest.rb new file mode 100644 index 0000000..5105881 --- /dev/null +++ b/db/migrate/20250620051905_add_lang_to_contest.rb @@ -0,0 +1,5 @@ +class AddLangToContest < ActiveRecord::Migration[8.0] + def change + add_column :contests, :lang, :string, default: 'en' + end +end diff --git a/db/schema.rb b/db/schema.rb index b501226..82b4f11 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/spec/factories/contests.rb b/spec/factories/contests.rb index 9017752..043a1a3 100644 --- a/spec/factories/contests.rb +++ b/spec/factories/contests.rb @@ -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) diff --git a/test/fixtures/contests.yml b/test/fixtures/contests.yml index 23349b3..bb492e0 100644 --- a/test/fixtures/contests.yml +++ b/test/fixtures/contests.yml @@ -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) diff --git a/test/models/contest_test.rb b/test/models/contest_test.rb index d387cea..c975a4f 100644 --- a/test/models/contest_test.rb +++ b/test/models/contest_test.rb @@ -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)