Add extra nav for settings & clean header buttons
All checks were successful
CI / scan_ruby (push) Successful in 20s
CI / scan_js (push) Successful in 14s
CI / lint (push) Successful in 13s
CI / test (push) Successful in 36s

This commit is contained in:
sto
2025-12-05 10:53:52 +01:00
parent e2c50515b1
commit 7bd1dce1ea
16 changed files with 239 additions and 131 deletions

View File

@@ -3,7 +3,7 @@ class ContestsController < ApplicationController
include ContestantsConcern include ContestantsConcern
before_action :set_contest, only: %i[ destroy show ] before_action :set_contest, only: %i[ destroy show ]
before_action :set_settings_contest, only: %i[ settings_general_edit settings_general_update settings_offline_edit settings_offline_update settings_categories_edit ] before_action :set_settings_contest, only: %i[ settings_general_edit settings_general_update settings_public_edit settings_public_update settings_onsite_edit settings_onsite_update settings_online_edit settings_online_update settings_categories_edit ]
before_action :offline_setup, only: %i[ offline_new offline_create offline_edit offline_update offline_completed ] before_action :offline_setup, only: %i[ offline_new offline_create offline_edit offline_update offline_completed ]
skip_before_action :require_authentication, only: %i[ scoreboard offline_new offline_create offline_edit offline_update offline_completed ] skip_before_action :require_authentication, only: %i[ scoreboard offline_new offline_create offline_edit offline_update offline_completed ]
@@ -24,7 +24,15 @@ class ContestsController < ApplicationController
authorize @contest authorize @contest
end end
def settings_offline_edit def settings_public_edit
authorize @contest
end
def settings_onsite_edit
authorize @contest
end
def settings_online_edit
authorize @contest authorize @contest
end end
@@ -42,13 +50,33 @@ class ContestsController < ApplicationController
end end
end end
def settings_offline_update def settings_public_update
authorize @contest authorize @contest
if @contest.update(settings_offline_params) if @contest.update(settings_public_params)
redirect_to "/contests/#{@contest.id}/settings/offline", notice: t("contests.edit.notice") redirect_to "/contests/#{@contest.id}/settings/public", notice: t("contests.edit.notice")
else else
render :settings_offline_edit, status: :unprocessable_entity render :settings_public_edit, status: :unprocessable_entity
end
end
def settings_onsite_update
authorize @contest
if @contest.update(settings_onsite_params)
redirect_to "/contests/#{@contest.id}/settings/onsite", notice: t("contests.edit.notice")
else
render :settings_onsite_edit, status: :unprocessable_entity
end
end
def settings_online_update
authorize @contest
if @contest.update(settings_online_params)
redirect_to "/contests/#{@contest.id}/settings/online", notice: t("contests.edit.notice")
else
render :settings_online_edit, status: :unprocessable_entity
end end
end end
@@ -210,10 +238,18 @@ class ContestsController < ApplicationController
end end
def settings_general_params def settings_general_params
params.expect(contest: [ :lang, :name, :duration, :public, :ranking_mode, :team, :allow_registration, :code ]) params.expect(contest: [ :lang, :name, :duration, :team, :allow_registration ])
end end
def settings_offline_params def settings_public_params
params.expect(contest: [ :public, :ranking_mode ])
end
def settings_onsite_params
params.expect(contest: [ :code ])
end
def settings_online_params
params.expect(contest: [ :offline_form ]) params.expect(contest: [ :offline_form ])
end end

View File

@@ -47,11 +47,27 @@ class ContestPolicy < ApplicationPolicy
edit? edit?
end end
def settings_offline_edit? def settings_public_edit?
edit? edit?
end end
def settings_offline_update? def settings_public_update?
edit?
end
def settings_onsite_edit?
edit?
end
def settings_onsite_update?
edit?
end
def settings_online_edit?
edit?
end
def settings_online_update?
edit? edit?
end end

View File

@@ -1,35 +1,3 @@
javascript:
async function copyExtensionUrlToClipboard() {
await navigator.clipboard.writeText("#{message_url}?token=#{@contest.generate_token_for(:token)}");
alert("#{t("contests.show.url_copied")}");
}
.row.mb-4
.col
- if @contest.public
a.btn.btn-success href="/public/#{@contest.slug}"
= t("contests.show.open_public_scoreboard")
- else
a.btn.btn-success.disabled
= t("contests.show.public_scoreboard_disabled")
- if @contest.offline_form && @contest.puzzles.length < 2
a.ms-3.btn.btn-success href="/public/#{@contest.slug}/offline"
= t("contests.show.open_offline_form")
- else
a.ms-3.btn.btn-success.disabled
= t("contests.show.offline_form_disabled")
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 .row
.col .col
ul.nav.nav-tabs.mb-4 ul.nav.nav-tabs.mb-4
@@ -43,11 +11,5 @@ javascript:
a.nav-link class=active_page(contest_messages_path(@contest)) href=contest_messages_path(@contest) a.nav-link class=active_page(contest_messages_path(@contest)) href=contest_messages_path(@contest)
= t("messages.plural").capitalize = t("messages.plural").capitalize
li.nav-item li.nav-item
a.nav-link class=active_page("/contests/#{@contest.id}/settings/general") href="/contests/#{@contest.id}/settings/general" a.nav-link class=active_page("/contests/#{@contest.id}/settings") href="/contests/#{@contest.id}/settings/general"
= t("contests.form.general") = t("contests.nav.settings")
li.nav-item
a.nav-link class=active_page("/contests/#{@contest.id}/settings/offline") href="/contests/#{@contest.id}/settings/offline"
= t("contests.form.offline")
li.nav-item
a.nav-link class=active_page("/contests/#{@contest.id}/settings/categories") href="/contests/#{@contest.id}/settings/categories"
= t("contests.form.categories")

View File

@@ -0,0 +1,18 @@
.row
.col
ul.nav.nav-tabs.mb-4
li.nav-item
a.nav-link class=active_page("/contests/#{@contest.id}/settings/general") href="/contests/#{@contest.id}/settings/general"
= t("contests.nav.general")
li.nav-item
a.nav-link class=active_page("/contests/#{@contest.id}/settings/public") href="/contests/#{@contest.id}/settings/public"
= t("contests.nav.public")
li.nav-item
a.nav-link class=active_page("/contests/#{@contest.id}/settings/onsite") href="/contests/#{@contest.id}/settings/onsite"
= t("contests.nav.onsite")
li.nav-item
a.nav-link class=active_page("/contests/#{@contest.id}/settings/online") href="/contests/#{@contest.id}/settings/online"
= t("contests.nav.online")
li.nav-item
a.nav-link class=active_page("/contests/#{@contest.id}/settings/categories") href="/contests/#{@contest.id}/settings/categories"
= t("contests.nav.categories")

View File

@@ -1,3 +1,10 @@
= render "params_nav"
.row
.col
.alert.alert-primary role="alert"
= t("contests.nav.categories_description")
= form_with model: Category, url: "/contests/#{@contest.id}/categories" do |form| = form_with model: Category, url: "/contests/#{@contest.id}/categories" do |form|
- if @contest.categories.size > 0 - if @contest.categories.size > 0
.row .row
@@ -24,6 +31,6 @@
= form.text_field :name, autocomplete: "off", value: nil, class: "form-control" = form.text_field :name, autocomplete: "off", value: nil, class: "form-control"
= form.label :name, class: "required" = form.label :name, class: "required"
= t("activerecord.attributes.category.new") = t("activerecord.attributes.category.new")
.row.mt-3 .row.mt-4
.col .col
= form.submit t("helpers.buttons.add"), class: "btn btn-primary" = form.submit t("helpers.buttons.add"), class: "btn btn-primary"

View File

@@ -1,3 +1,5 @@
= render "params_nav"
= form_with model: @contest, url: "/contests/#{@contest.id}/settings/general" do |form| = form_with model: @contest, url: "/contests/#{@contest.id}/settings/general" do |form|
.row.mt-2.mb-3 .row.mt-2.mb-3
.col .col
@@ -15,22 +17,12 @@
.form-floating .form-floating
= form.select :lang, Languages::AVAILABLE_LANGUAGES.map { |lang| [ lang[:name], lang[:id] ] }, {}, class: "form-select" = form.select :lang, Languages::AVAILABLE_LANGUAGES.map { |lang| [ lang[:name], lang[:id] ] }, {}, class: "form-select"
= form.label :lang = form.label :lang
.row.mb-3
.col
.form-floating
= form.select :ranking_mode, Ranking::AVAILABLE_RANKING_MODES.map { |mode| [ mode[:name], mode[:id] ] }, {}, class: "form-select"
= form.label :ranking_mode
.row.mt-2.mb-3 .row.mt-2.mb-3
.col .col
.form-floating .form-floating
= form.text_field :code, autocomplete: "off", class: "form-control" = form.text_field :code, autocomplete: "off", class: "form-control"
= form.label :code, class: "required" = form.label :code, class: "required"
.form-text = t("activerecord.attributes.contest.code_description") .form-text = t("activerecord.attributes.contest.code_description")
.row.mt-4.mb-3
.col
.form-check.form-switch
= form.check_box :public, class: "form-check-input"
= form.label :public
.row.mb-3 style="display: none" .row.mb-3 style="display: none"
.col .col
.form-check.form-switch .form-check.form-switch

View File

@@ -1,25 +0,0 @@
- if @contest.puzzles.length > 1
.row
.col
.alert.alert-warning
= t("contests.form.offline_single_puzzle_warning")
- if @contest.puzzles.length <= 1
= form_with model: @contest, url: "/contests/#{@contest.id}/settings/offline" do |form|
.row.mt-2.mb-3
.col
- if @contest.puzzles.length <= 1
.form-check.form-switch
= form.check_box :offline_form, class: "form-check-input"
= form.label :offline_form
.form-text = t("activerecord.attributes.contest.offline_form_warning")
.form-text = t("activerecord.attributes.contest.offline_form_description")
- else
.form-check.form-switch
= form.check_box :offline_form_fake, class: "form-check-input", disabled: true
= form.label :offline_form
.form-text = t("activerecord.attributes.contest.offline_form_warning")
.form-text = t("activerecord.attributes.contest.offline_form_description")
.row.mt-4
.col
= form.submit t("helpers.buttons.update"), class: "btn btn-primary"

View File

@@ -0,0 +1,46 @@
= render "params_nav"
javascript:
async function copyExtensionUrlToClipboard() {
await navigator.clipboard.writeText("#{message_url}?token=#{@contest.generate_token_for(:token)}");
alert("#{t("contests.show.url_copied")}");
}
.row.mb-4.mt-2
.col
- if @contest.offline_form && @contest.puzzles.length < 2
a.btn.btn-success href="/public/#{@contest.slug}/offline"
= t("contests.show.open_offline_form")
- else
a.btn.btn-success.disabled
= t("contests.show.offline_form_disabled")
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")
= form_with model: @contest, url: "/contests/#{@contest.id}/settings/online" do |form|
.row.mt-2.mb-3
.col
- if @contest.puzzles.length <= 1
.form-check.form-switch
= form.check_box :offline_form, class: "form-check-input"
= form.label :offline_form
.form-text = t("activerecord.attributes.contest.offline_form_warning")
.form-text = t("activerecord.attributes.contest.offline_form_description")
- else
.form-check.form-switch
= form.check_box :offline_form_fake, class: "form-check-input", disabled: true
= form.label :offline_form
.form-text = t("activerecord.attributes.contest.offline_form_warning")
.form-text = t("activerecord.attributes.contest.offline_form_description")
.row.mt-4
.col
= form.submit t("helpers.buttons.update"), class: "btn btn-primary"

View File

@@ -0,0 +1,12 @@
= render "params_nav"
= form_with model: @contest, url: "/contests/#{@contest.id}/settings/general" do |form|
.row.mt-2.mb-3
.col
.form-floating
= form.text_field :code, autocomplete: "off", class: "form-control"
= form.label :code, class: "required"
.form-text = t("activerecord.attributes.contest.code_description")
.row.mt-4
.col
= form.submit t("helpers.buttons.update"), class: "btn btn-primary"

View File

@@ -0,0 +1,17 @@
= render "params_nav"
= form_with model: @contest, url: "/contests/#{@contest.id}/settings/public" do |form|
.row.mt-2
.col
.form-check.form-switch
= form.check_box :public, class: "form-check-input"
= form.label :public
.row.mt-3
.col
.form-floating
= form.select :ranking_mode, Ranking::AVAILABLE_RANKING_MODES.map { |mode| [ mode[:name], mode[:id] ] }, {}, class: "form-select"
= form.label :ranking_mode
.row.mt-4
.col
= form.submit t("helpers.buttons.update"), class: "btn btn-primary"

View File

@@ -5,7 +5,7 @@ html
body body
.container.mt-5 .container.mt-5
- if @current_user - if @current_user
.float-end style="margin-top: -8px;" .float-end style="margin-top: -5px;"
nav.navbar.bg-body-primary nav.navbar.bg-body-primary
- if @current_user.admin - if @current_user.admin
a.navbar-brand href=users_path class="btn btn-light" style="margin-right: 0" a.navbar-brand href=users_path class="btn btn-light" style="margin-right: 0"
@@ -43,12 +43,19 @@ html
.toast-body .toast-body
= msg = msg
h1.mb-4 h1.mb-5
- if @contest && @contest.id.present? - if @contest && @contest.id.present?
= @contest.name = @contest.name
- if active_page("/public") == "active" && @action_path - if active_page("/public") == "active" && @action_path
a.ms-4.btn.btn-primary href=@action_path style="margin-top: -6px" a.ms-4.btn.btn-primary href=@action_path style="margin-top: -6px"
= t("helpers.buttons.refresh") = t("helpers.buttons.refresh")
- if active_page("/contests") == "active"
- if @contest.public
a.ms-4.btn.btn-success href="/public/#{@contest.slug}" style="margin-top: -6px;"
= t("contests.show.open_public_scoreboard")
- else
a.ms-4.btn.btn-success.disabled style="margin-top: -6px;"
= t("contests.show.public_scoreboard_disabled")
- else - else
= @title = @title

View File

@@ -3,6 +3,8 @@
.row.mb-4 .row.mb-4
.col .col
.alert.alert-primary
= t("messages.index.info")
- if @messages.length == 0 - if @messages.length == 0
.alert.alert-warning .alert.alert-warning
= t("messages.index.no_messages") = t("messages.index.no_messages")

View File

@@ -5,36 +5,38 @@
.col .col
a.btn.btn-primary href=new_contest_puzzle_path(@contest) style="margin-top: -3px" a.btn.btn-primary href=new_contest_puzzle_path(@contest) style="margin-top: -3px"
| + #{t("helpers.buttons.add")} | + #{t("helpers.buttons.add")}
table.table.table-striped.table-hover
thead .d-flex.flex-column style="overflow-y: auto"
tr table.table.table-striped.table-hover
th thead
= t("activerecord.attributes.puzzle.image") tr
th th
= t("activerecord.attributes.puzzle.name") = t("activerecord.attributes.puzzle.image")
th th
= t("activerecord.attributes.puzzle.brand") = t("activerecord.attributes.puzzle.name")
th th
= t("activerecord.attributes.puzzle.pieces") = t("activerecord.attributes.puzzle.brand")
th th
= t("activerecord.attributes.puzzle.hidden") = t("activerecord.attributes.puzzle.pieces")
tbody th
- @puzzles.each do |puzzle| = t("activerecord.attributes.puzzle.hidden")
tr.align-middle scope="row" tbody
td - @puzzles.each do |puzzle|
= image_tag(puzzle.image, class: "img-fluid", style: "max-height: 128px;") if puzzle.image.attached? tr.align-middle scope="row"
td td
= puzzle.name = image_tag(puzzle.image, class: "img-fluid", style: "max-height: 128px;") if puzzle.image.attached?
td td
= puzzle.brand = puzzle.name
td td
= puzzle.pieces = puzzle.brand
td td
- if puzzle.hidden? = puzzle.pieces
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-square" viewBox="0 0 16 16"> td
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/> - if puzzle.hidden?
<path d="M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"/> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-square" viewBox="0 0 16 16">
</svg> <path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
td <path d="M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z"/>
a.btn.btn-sm.btn-secondary href=edit_contest_puzzle_path(@contest, puzzle) </svg>
= t("helpers.buttons.edit") td
a.btn.btn-sm.btn-secondary href=edit_contest_puzzle_path(@contest, puzzle)
= t("helpers.buttons.edit")

View File

@@ -193,14 +193,19 @@ en:
notice: Contest updated notice: Contest updated
title: Edit contest settings title: Edit contest settings
form: form:
categories: Participant categories
general: General parameters
offline: Offline participation
offline_single_puzzle_warning: This is not available for contests with more than one puzzle offline_single_puzzle_warning: This is not available for contests with more than one puzzle
index: index:
title: Welcome %{username}! title: Welcome %{username}!
manage_contests: Manage my contests manage_contests: Manage my contests
new_contest: Create a new contest new_contest: Create a new contest
nav:
categories: Participant categories
categories_description: Once you add categories, you will be able to assign them to participants on their profiles, and a filter for categories will be available on the public scoreboard
general: General
online: Online contests
onsite: Onsite contests
public: Public scoreboard
settings: Settings
new: new:
notice: Contest added notice: Contest added
title: New jigsaw puzzle contest title: New jigsaw puzzle contest
@@ -287,10 +292,11 @@ en:
rank: Rank rank: Rank
lib: lib:
ranking: ranking:
actual: First by number of pieces assembled, then by time actual: First by number of pieces assembled, then by time (recommended if unsure)
theorical: By time only (projected time calculated with the ppm count) theorical: By time only (projected time calculated with the ppm count)
messages: messages:
index: index:
info: This section is only used for contests that rely on the connection with Google Meet
no_messages: No messages received yet no_messages: No messages received yet
convert: convert:
title: New completion title: New completion

View File

@@ -164,14 +164,19 @@ fr:
notice: Concours modifié notice: Concours modifié
title: Paramètres du concours title: Paramètres du concours
form: form:
categories: Catégories de participant.e.s
general: Paramètres généraux
offline: Participation hors-ligne
offline_single_puzzle_warning: Ce n'est pas activable pour les concours avec plusieurs puzzles offline_single_puzzle_warning: Ce n'est pas activable pour les concours avec plusieurs puzzles
index: index:
title: Bienvenue %{username} ! title: Bienvenue %{username} !
manage_contests: Mes concours de puzzle manage_contests: Mes concours de puzzle
new_contest: Créer un nouveau concours new_contest: Créer un nouveau concours
nav:
categories: Catégories de participant.e.s
categories_description: Après avoir ajouté des catégories, elles pourront être attributées aux participant.e.s sur leurs profils, et un filtre sera disponible sur le classement public
general: Général
online: Concours en ligne
onsite: Concours en présentiel
public: Classement public
settings: Paramètres
new: new:
notice: Concours ajouté notice: Concours ajouté
title: Nouveau concours title: Nouveau concours
@@ -258,10 +263,11 @@ fr:
rank: Rang rank: Rang
lib: lib:
ranking: ranking:
actual: Par nombre de pièces assemblées, puis par temps actual: Par nombre de pièces assemblées, puis par temps (recommandé)
theorical: Par temps uniquement (temps projeté calculé à partir de la vitesse d'assemblage) theorical: Par temps uniquement (temps projeté calculé à partir de la vitesse d'assemblage)
messages: messages:
index: index:
info: Cette section n'est pertinente que pour les concours en ligne qui utilisent la connexion depuis Google Meet
no_messages: Pas de messages reçus pour le moment no_messages: Pas de messages reçus pour le moment
convert: convert:
title: Ajout d'une complétion title: Ajout d'une complétion

View File

@@ -11,8 +11,12 @@ Rails.application.routes.draw do
resources :contests do resources :contests do
get "settings/general", to: "contests#settings_general_edit" get "settings/general", to: "contests#settings_general_edit"
patch "settings/general", to: "contests#settings_general_update" patch "settings/general", to: "contests#settings_general_update"
get "settings/offline", to: "contests#settings_offline_edit" get "settings/public", to: "contests#settings_public_edit"
patch "settings/offline", to: "contests#settings_offline_update" patch "settings/public", to: "contests#settings_public_update"
get "settings/onsite", to: "contests#settings_onsite_edit"
patch "settings/onsite", to: "contests#settings_onsite_update"
get "settings/online", to: "contests#settings_online_edit"
patch "settings/online", to: "contests#settings_online_update"
get "settings/categories", to: "contests#settings_categories_edit" get "settings/categories", to: "contests#settings_categories_edit"
resources :categories, only: [ :create, :destroy ] resources :categories, only: [ :create, :destroy ]
resources :completions resources :completions