diff options
author | Marius Peter <marius.peter@tutanota.com> | 2024-12-29 15:14:43 +0100 |
---|---|---|
committer | Marius Peter <marius.peter@tutanota.com> | 2024-12-29 15:14:43 +0100 |
commit | be2a93525069de2dfa3c23b0c23e7a9f7ad4c03d (patch) | |
tree | b5493e9d35d024ce7be072ec2168b4a98ba0e63f /app |
First commit.
Diffstat (limited to 'app')
59 files changed, 798 insertions, 0 deletions
diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/assets/images/.keep diff --git a/app/assets/images/blason_Savoie.png b/app/assets/images/blason_Savoie.png Binary files differnew file mode 100644 index 0000000..51761d0 --- /dev/null +++ b/app/assets/images/blason_Savoie.png diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000..d7fc4f9 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,27 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + * With Propshaft, assets are served efficiently without preprocessing steps. You can still include + * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard + * cascading order, meaning styles declared later in the document or manifest will override earlier ones, + * depending on specificity. + * + * Consider organizing styles into separate files for maintainability. + */ + +body { + margin: 0; + font-family: sans-serif; + background: moccasin; +} + +main { + margin: 0 auto; + padding: 0 1em; + max-width: 50em; +} + +h1 { + text-align: center; + /* font-family: 'Old London', sans-serif; */ +} diff --git a/app/assets/stylesheets/components/code_of_honor.css b/app/assets/stylesheets/components/code_of_honor.css new file mode 100644 index 0000000..dbbd267 --- /dev/null +++ b/app/assets/stylesheets/components/code_of_honor.css @@ -0,0 +1,11 @@ +#code-of-honor { + width: 50%; + margin: 1em auto; + padding: 1em; + color: white; + + border-radius: 1em; +} +#code-of-honor.accepted { background-color: forestgreen; } +#code-of-honor.rejected { background-color: firebrick; } + diff --git a/app/assets/stylesheets/components/footer.css b/app/assets/stylesheets/components/footer.css new file mode 100644 index 0000000..de96a5c --- /dev/null +++ b/app/assets/stylesheets/components/footer.css @@ -0,0 +1,6 @@ +footer { + text-align: center; + margin: 3em 0 0; + color: dimgrey; + +} diff --git a/app/assets/stylesheets/components/nav_top.css b/app/assets/stylesheets/components/nav_top.css new file mode 100644 index 0000000..a3247a4 --- /dev/null +++ b/app/assets/stylesheets/components/nav_top.css @@ -0,0 +1,40 @@ +nav#top { + width: 100%; + background-color: firebrick; + display: flex; + justify-content: space-between; + align-items: center; + + ul { + display: flex; + flex-wrap: wrap; + list-style-type: none; + padding: 0; + margin: 0; + } + + li { + padding: 12px 0; + } + + a { + padding: 12px; + color: white; + text-decoration: none; + } + + a:hover { + background-color: tomato; + } + + #authentication { + display: flex; + flex-wrap: wrap; + justify-content: right; + } + + button { + margin: 0.5rem 0.5rem 0.5rem 0; + } +} + diff --git a/app/assets/stylesheets/components/notifications.css b/app/assets/stylesheets/components/notifications.css new file mode 100644 index 0000000..93d8cc2 --- /dev/null +++ b/app/assets/stylesheets/components/notifications.css @@ -0,0 +1,18 @@ +#notifications { + max-width: 30rem; + margin: 1em auto; +} + +.notice, .alert { + margin: 0 1em; + padding: 1em; + border-radius: 1em; +} + +.notice { + background: lightblue; +} + +.alert { + background: tomato; +} diff --git a/app/assets/stylesheets/pages/home.css b/app/assets/stylesheets/pages/home.css new file mode 100644 index 0000000..5b2e6a9 --- /dev/null +++ b/app/assets/stylesheets/pages/home.css @@ -0,0 +1,4 @@ +#blason { + margin: 0 auto; + display: block; +} diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..4264c74 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,16 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + set_current_user || reject_unauthorized_connection + end + + private + def set_current_user + if session = Session.find_by(id: cookies.signed[:session_id]) + self.current_user = session.user + end + end + end +end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb new file mode 100644 index 0000000..695c2ca --- /dev/null +++ b/app/controllers/admin/dashboard_controller.rb @@ -0,0 +1,13 @@ +class Admin::DashboardController < ApplicationController + def index + @tartiflettes = Tartiflette.includes(:scores) + end + + private + + def require_admin + unless logged_in? && current_user.admin? + redirect_to root_path, alert: "Access denied." + end + end +end diff --git a/app/controllers/admin/scores_controller.rb b/app/controllers/admin/scores_controller.rb new file mode 100644 index 0000000..b4755e9 --- /dev/null +++ b/app/controllers/admin/scores_controller.rb @@ -0,0 +1,14 @@ +class Admin::ScoresController < ApplicationController + def export + csv_data = TartifletteScoreExportService.generate_csv + send_data csv_data, filename: "scores-#{Date.today}.csv" + end + + private + + def require_admin + unless logged_in? && current_user.admin? + redirect_to root_path, alert: "Access denied." + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..94e7183 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,5 @@ +class ApplicationController < ActionController::Base + include Authentication + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern +end diff --git a/app/controllers/code_of_honor_controller.rb b/app/controllers/code_of_honor_controller.rb new file mode 100644 index 0000000..d0d9044 --- /dev/null +++ b/app/controllers/code_of_honor_controller.rb @@ -0,0 +1,12 @@ +class CodeOfHonorController < ApplicationController + allow_unauthenticated_access + + def toggle + session[:agreed_to_code_of_honor] = !session[:agreed_to_code_of_honor] + if session[:agreed_to_code_of_honor] + redirect_to root_path, notice: "Vous acceptez le code d'honneur." + else + redirect_to root_path, alert: "Vous n'acceptez pas le code d'honneur." + end + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/controllers/concerns/.keep diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb new file mode 100644 index 0000000..3538f48 --- /dev/null +++ b/app/controllers/concerns/authentication.rb @@ -0,0 +1,52 @@ +module Authentication + extend ActiveSupport::Concern + + included do + before_action :require_authentication + helper_method :authenticated? + end + + class_methods do + def allow_unauthenticated_access(**options) + skip_before_action :require_authentication, **options + end + end + + private + def authenticated? + resume_session + end + + def require_authentication + resume_session || request_authentication + end + + def resume_session + Current.session ||= find_session_by_cookie + end + + def find_session_by_cookie + Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id] + end + + def request_authentication + session[:return_to_after_authenticating] = request.url + redirect_to new_session_path + end + + def after_authentication_url + session.delete(:return_to_after_authenticating) || root_url + end + + def start_new_session_for(user) + user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| + Current.session = session + cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax } + end + end + + def terminate_session + Current.session.destroy + cookies.delete(:session_id) + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..cd27f53 --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,7 @@ +class HomeController < ApplicationController + allow_unauthenticated_access + + def index + @tartiflettes = Tartiflette.all + end +end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 0000000..0c4b4a8 --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,33 @@ +class PasswordsController < ApplicationController + allow_unauthenticated_access + before_action :set_user_by_token, only: %i[ edit update ] + + def new + end + + def create + if user = User.find_by(email_address: params[:email_address]) + PasswordsMailer.reset(user).deliver_later + end + + redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)." + end + + def edit + end + + def update + if @user.update(params.permit(:password, :password_confirmation)) + redirect_to new_session_path, notice: "Password has been reset." + else + redirect_to edit_password_path(params[:token]), alert: "Passwords did not match." + end + end + + private + def set_user_by_token + @user = User.find_by_password_reset_token!(params[:token]) + rescue ActiveSupport::MessageVerifier::InvalidSignature + redirect_to new_password_path, alert: "Password reset link is invalid or has expired." + end +end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb new file mode 100644 index 0000000..d2a6822 --- /dev/null +++ b/app/controllers/registrations_controller.rb @@ -0,0 +1,21 @@ +class RegistrationsController < ApplicationController + def new + @user = User.new + end + + def create + @user = User.new(user_params) + if @user.save + start_new_session_for @user + redirect_to root_path, notice: "Successfully signed up!" + else + render :new + end + end + + private + + def user_params + params.require(:user).permit(:email_address, :password, :password_confirmation) + end +end diff --git a/app/controllers/scores_controller.rb b/app/controllers/scores_controller.rb new file mode 100644 index 0000000..650c4e6 --- /dev/null +++ b/app/controllers/scores_controller.rb @@ -0,0 +1,49 @@ +class ScoresController < ApplicationController + allow_unauthenticated_access + before_action :set_tartiflette, only: [ :new, :create, :edit_all, :update_all ] + before_action :scores_params, only: [ :create, :update_all ] + + def new + end + + def create + if TartifletteScoringService.scored?(@tartiflette, session) + redirect_to root_path, alert: "Vous avez déja noté cette tartiflette." + return + end + + TartifletteScoringService.submit_scores(@tartiflette, scores_params, session) + redirect_to root_path, + notice: "Vos scores pour la tartiflette #{@tartiflette.scoring_id} ont été enregistrés." + rescue StandardError => e + redirect_to root_path, + status: :unprocessable_entity, + alert: "Erreur lors de l'enregistrement de vos scores : #{e.message}" + end + + def edit_all + @scores = @tartiflette.scores + end + + def update_all + scores_params.each do |score_id, score_params| + score = @tartiflette.scores.find(score_id) + score.update!(value: score_params[:value]) + end + redirect_to root_path, + notice: "Vos scores pour la tartiflette #{@tartiflette.scoring_id} ont été mis à jour." + rescue StandardError => e + redirect_to edit_tartiflette_scores_path(@tartiflette), + alert: "Erreur lors de l'enregistrement de vos scores : #{e.message}" + end + + private + + def set_tartiflette + @tartiflette = Tartiflette.find(params[:tartiflette_id]) + end + + def scores_params + params.require(:scores).permit!.to_h + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..9785c92 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,21 @@ +class SessionsController < ApplicationController + allow_unauthenticated_access only: %i[ new create ] + rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." } + + def new + end + + def create + if user = User.authenticate_by(params.permit(:email_address, :password)) + start_new_session_for user + redirect_to after_authentication_url + else + redirect_to new_session_path, alert: "Try another email address or password." + end + end + + def destroy + terminate_session + redirect_to new_session_path + end +end diff --git a/app/controllers/tartiflettes_controller.rb b/app/controllers/tartiflettes_controller.rb new file mode 100644 index 0000000..cbea402 --- /dev/null +++ b/app/controllers/tartiflettes_controller.rb @@ -0,0 +1,26 @@ +class TartiflettesController < ApplicationController + before_action :set_tartiflette, only: [ :show ] + + def index + @tartiflettes = Tartiflette.all + end + + def show + end + + def new + @tartiflette = Tartiflette.new + end + + private + + def tartiflette_params + params.require(:tartiflette) + end + + def set_tartiflette + @tartiflette = Tartiflette.find(params[:id]) + rescue ActiveRecord::RecordNotFound + redirect_to root_path, alert: "Tartiflette introuvable." + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 0000000..23de56a --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..3c34c81 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/mailers/passwords_mailer.rb b/app/mailers/passwords_mailer.rb new file mode 100644 index 0000000..4f0ac7f --- /dev/null +++ b/app/mailers/passwords_mailer.rb @@ -0,0 +1,6 @@ +class PasswordsMailer < ApplicationMailer + def reset(user) + @user = user + mail subject: "Reset your password", to: user.email_address + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..b63caeb --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/models/concerns/.keep diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 0000000..2bef56d --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,4 @@ +class Current < ActiveSupport::CurrentAttributes + attribute :session + delegate :user, to: :session, allow_nil: true +end diff --git a/app/models/score.rb b/app/models/score.rb new file mode 100644 index 0000000..5147ce0 --- /dev/null +++ b/app/models/score.rb @@ -0,0 +1,6 @@ +class Score < ApplicationRecord + belongs_to :tartiflette + belongs_to :scoring_criterium + + validates :value, presence: true, inclusion: { in: 1..5 } +end diff --git a/app/models/scoring_criterium.rb b/app/models/scoring_criterium.rb new file mode 100644 index 0000000..a94ad77 --- /dev/null +++ b/app/models/scoring_criterium.rb @@ -0,0 +1,9 @@ +class ScoringCriterium < ApplicationRecord + has_many :scores, dependent: :destroy + + validates :name, :category, presence: true + + def self.grouped_by_category + ScoringCriterium.all.group_by(&:category) + end +end diff --git a/app/models/session.rb b/app/models/session.rb new file mode 100644 index 0000000..cf376fb --- /dev/null +++ b/app/models/session.rb @@ -0,0 +1,3 @@ +class Session < ApplicationRecord + belongs_to :user +end diff --git a/app/models/tartiflette.rb b/app/models/tartiflette.rb new file mode 100644 index 0000000..4abfe74 --- /dev/null +++ b/app/models/tartiflette.rb @@ -0,0 +1,3 @@ +class Tartiflette < ApplicationRecord + has_many :scores +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..c88d5b0 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,6 @@ +class User < ApplicationRecord + has_secure_password + has_many :sessions, dependent: :destroy + + normalizes :email_address, with: ->(e) { e.strip.downcase } +end diff --git a/app/services/tartiflette_score_export_service.rb b/app/services/tartiflette_score_export_service.rb new file mode 100644 index 0000000..26334a0 --- /dev/null +++ b/app/services/tartiflette_score_export_service.rb @@ -0,0 +1,19 @@ +require "csv" + +class TartifletteScoreExportService + def self.generate_csv + CSV.generate(headers: true) do |csv| + csv << [ "Identifiant", "Critère", "Score", "Création" ] + Tartiflette.all.each do |tartiflette| + tartiflette.scores.each do |score| + csv << [ + tartiflette.scoring_id, + score.scoring_criterium.name, + score.value, + score.created_at + ] + end + end + end + end +end diff --git a/app/services/tartiflette_scoring_service.rb b/app/services/tartiflette_scoring_service.rb new file mode 100644 index 0000000..3514eb1 --- /dev/null +++ b/app/services/tartiflette_scoring_service.rb @@ -0,0 +1,54 @@ +class TartifletteScoringService + def self.scored?(tartiflette, session) + session[:scored_tartiflettes]&.include?(tartiflette.id) + end + + def self.mark_as_scored(tartiflette, session) + session[:scored_tartiflettes] ||= [] + unless scored?(tartiflette, session) + session[:scored_tartiflettes] << tartiflette.id + end + end + + def self.submit_scores(tartiflette, scores, session) + scores.each do |criterium_id, value| + Score.create!( + tartiflette: tartiflette, + scoring_criterium_id: criterium_id, + value: value[:value] + ) + end + mark_as_scored(tartiflette, session) + end + + def self.average_score(tartiflette) + tartiflette.scores.average(:value).to_f + end + + def self.average_score_by_category(tartiflette) + tartiflette + .scores + .group_by { |score| score.scoring_criterium.category } + .transform_values do |scores| + (scores.sum(&:value).to_f / scores.size).round(2) + end + end + + def self.total_score_by_category(tartiflette) + tartiflette + .scores + .group_by { |score| score.scoring_criterium.category } + .transform_values do |scores| + (scores.sum(&:value).to_f / scores.size).round(2) + end + end + + def self.leaderboard + Tartiflette + .joins(:scores) + .select("tartiflettes.*, SUM(scores.value) AS total_score") + .group("tartiflettes.id") + .order("total_score DESC") + .map { |tartiflette| [ tartiflette, tartiflette.total_score.to_f ] } + end +end diff --git a/app/views/admin/dashboard/index.html.erb b/app/views/admin/dashboard/index.html.erb new file mode 100644 index 0000000..30cf866 --- /dev/null +++ b/app/views/admin/dashboard/index.html.erb @@ -0,0 +1,10 @@ +<h1>Administrateur</h1> + +<!-- <h2>Tartiflettes</h2> --> + +<p> + <%= button_to "Déconnexion", + session_path(session), + method: :delete %> +</p> +<p><%= link_to "Télécharger tous les scores en format CSV", admin_scores_export_path %></p> diff --git a/app/views/admin/dashboard/tmp b/app/views/admin/dashboard/tmp new file mode 100644 index 0000000..986fc28 --- /dev/null +++ b/app/views/admin/dashboard/tmp @@ -0,0 +1,19 @@ +<% ScoringCriterium.grouped_by_category.each do |category, criteria| %> + <h3><%= category.capitalize %></h3> + <table> + <thead> + <tr> + <th>Numéro</th> + <th>Score</th> + </tr> + </thead> + <tbody> + <% @tartiflettes.each do |tartiflette| %> + <tr> + <td><%= tartiflette.scoring_id %></td> + <td><%= tartiflette.scores.where(&:scoring_criterium.include? criteria) %></td> + </tr> + <% end %> + </tbody> + </table> +<% end %> diff --git a/app/views/home/_code_of_honor.html.erb b/app/views/home/_code_of_honor.html.erb new file mode 100644 index 0000000..517a73e --- /dev/null +++ b/app/views/home/_code_of_honor.html.erb @@ -0,0 +1,14 @@ +<div id="code-of-honor" class="<%= session[:agreed_to_code_of_honor] ? 'accepted' : 'rejected' %>"> + <h2>Code d'honneur</h2> + <p> + Tout Tartifleur s'engage à voter dans le respect de la + tradition de la WTT. Il ou elle se doit de voter en toute + honnêteté intellectuelle afin de favoriser un résultat mérité + ! + </p> + <% if session[:agreed_to_code_of_honor] %> + <%= button_to "Renéguer", toggle_code_of_honor_path %> + <% else %> + <%= button_to "Accepter", toggle_code_of_honor_path %> + <% end %> +</div> diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 0000000..ca8f3a3 --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,42 @@ +<h1>World Tartiflette Tour 2024</h1> + +<%= image_tag("blason_Savoie.png", :alt => "blason de la Savoie", id: "blason", width: 80) %> + +<%= render "code_of_honor" %> + +<h2>Noter les Tartiflettes</h2> +<ul> + <% @tartiflettes.each do |tartiflette| %> + <li><%= tartiflette.scoring_id %> + <% if session[:agreed_to_code_of_honor] %> + <% if TartifletteScoringService.scored?(tartiflette, session) %> + <%= link_to "modifier", tartiflette_edit_scores_path(tartiflette) %> + <% else %> + <%= link_to "noter", new_tartiflette_score_path(tartiflette) %> + <% end %> + <% end %> + </li> + <% end %> +</ul> +<!-- + <h2>Résultats</h2> + <table> + <thead> + <tr> + <th>Tartiflette</th> + <th>Points</th> + </tr> + </thead> + <tbody> + <% TartifletteScoringService.leaderboard.each do |tartiflette, total_score| %> + <tr> + <td><%= tartiflette.scoring_id %></td> + <td><%= total_score %></td> + </tr> + <% end %> + </tbody> + </table> +--> +<p> + <%= link_to "Admin", admin_dashboard_path %> +</p> diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb new file mode 100644 index 0000000..bba04fb --- /dev/null +++ b/app/views/layouts/_footer.html.erb @@ -0,0 +1,3 @@ +<footer> + <p>© <%= Date.current.year %> World Tartiflette Tour</p> +</footer> diff --git a/app/views/layouts/_notifications.html.erb b/app/views/layouts/_notifications.html.erb new file mode 100644 index 0000000..3e1eb83 --- /dev/null +++ b/app/views/layouts/_notifications.html.erb @@ -0,0 +1,8 @@ +<div id="notifications"> + <% if alert %> + <p class="alert"><%= alert %></p> + <% end %> + <% if notice %> + <p class="notice"><%= notice %></p> + <% end %> +</div> diff --git a/app/views/layouts/_topnav.html.erb b/app/views/layouts/_topnav.html.erb new file mode 100644 index 0000000..f93ed99 --- /dev/null +++ b/app/views/layouts/_topnav.html.erb @@ -0,0 +1,5 @@ +<nav id="top"> + <ul> + <li><%= link_to "Accueil", root_path %></li> + </ul> +</nav> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..680dec1 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> + <head> + <title><%= content_for(:title) || "WTT" %></title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <meta name="apple-mobile-web-app-capable" content="yes"> + <meta name="mobile-web-app-capable" content="yes"> + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> + <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> + + <%= favicon_link_tag "blason_Savoie.png" %> + <!-- <link rel="icon" href="/icon.png" type="image/png"> + <link rel="icon" href="/icon.svg" type="image/svg+xml"> + <link rel="apple-touch-icon" href="/icon.png"> --> + + <%# Includes all stylesheet files in app/assets/stylesheets %> + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> + </head> + + <body> + <%= render "layouts/topnav" %> + <%= render "layouts/notifications" %> + <main> + <%= yield %> + </main> + <%= render "layouts/footer" %> + </body> +</html> diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..3aac900 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <style> + /* Email styles need to be inline */ + </style> + </head> + + <body> + <%= yield %> + </body> +</html> diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb new file mode 100644 index 0000000..9f0c87c --- /dev/null +++ b/app/views/passwords/edit.html.erb @@ -0,0 +1,9 @@ +<h1>Update your password</h1> + +<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> + +<%= form_with url: password_path(params[:token]), method: :put do |form| %> + <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %><br> + <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %><br> + <%= form.submit "Save" %> +<% end %> diff --git a/app/views/passwords/new.html.erb b/app/views/passwords/new.html.erb new file mode 100644 index 0000000..44efb2b --- /dev/null +++ b/app/views/passwords/new.html.erb @@ -0,0 +1,8 @@ +<h1>Forgot your password?</h1> + +<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> + +<%= form_with url: passwords_path do |form| %> + <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br> + <%= form.submit "Email reset instructions" %> +<% end %> diff --git a/app/views/passwords_mailer/reset.html.erb b/app/views/passwords_mailer/reset.html.erb new file mode 100644 index 0000000..4a06619 --- /dev/null +++ b/app/views/passwords_mailer/reset.html.erb @@ -0,0 +1,4 @@ +<p> + You can reset your password within the next 15 minutes on + <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>. +</p> diff --git a/app/views/passwords_mailer/reset.text.erb b/app/views/passwords_mailer/reset.text.erb new file mode 100644 index 0000000..2cf03fc --- /dev/null +++ b/app/views/passwords_mailer/reset.text.erb @@ -0,0 +1,2 @@ +You can reset your password within the next 15 minutes on this password reset page: +<%= edit_password_url(@user.password_reset_token) %> diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb new file mode 100644 index 0000000..883d71c --- /dev/null +++ b/app/views/pwa/manifest.json.erb @@ -0,0 +1,22 @@ +{ + "name": "Wtt", + "icons": [ + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "/icon.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "maskable" + } + ], + "start_url": "/", + "display": "standalone", + "scope": "/", + "description": "Wtt.", + "theme_color": "red", + "background_color": "red" +} diff --git a/app/views/pwa/service-worker.js b/app/views/pwa/service-worker.js new file mode 100644 index 0000000..b3a13fb --- /dev/null +++ b/app/views/pwa/service-worker.js @@ -0,0 +1,26 @@ +// Add a service worker for processing Web Push notifications: +// +// self.addEventListener("push", async (event) => { +// const { title, options } = await event.data.json() +// event.waitUntil(self.registration.showNotification(title, options)) +// }) +// +// self.addEventListener("notificationclick", function(event) { +// event.notification.close() +// event.waitUntil( +// clients.matchAll({ type: "window" }).then((clientList) => { +// for (let i = 0; i < clientList.length; i++) { +// let client = clientList[i] +// let clientPath = (new URL(client.url)).pathname +// +// if (clientPath == event.notification.data.path && "focus" in client) { +// return client.focus() +// } +// } +// +// if (clients.openWindow) { +// return clients.openWindow(event.notification.data.path) +// } +// }) +// ) +// }) diff --git a/app/views/registrations/new.html.erb b/app/views/registrations/new.html.erb new file mode 100644 index 0000000..9d9f9bc --- /dev/null +++ b/app/views/registrations/new.html.erb @@ -0,0 +1,17 @@ +<h1>Sign Up</h1> + +<%= form_with model: @user, url: registration_path, method: :post, local: true do |f| %> + <div> + <%= f.label :email_address %> + <%= f.email_field :email_address, required: true %> + </div> + <div> + <%= f.label :password %> + <%= f.password_field :password, required: true %> + </div> + <div> + <%= f.label :password_confirmation %> + <%= f.password_field :password_confirmation, required: true %> + </div> + <%= f.submit "Sign Up" %> +<% end %> diff --git a/app/views/scores/_form.html.erb b/app/views/scores/_form.html.erb new file mode 100644 index 0000000..9cbcec7 --- /dev/null +++ b/app/views/scores/_form.html.erb @@ -0,0 +1,19 @@ +<%= form_with url: form_url, method: form_method, local: true do |f| %> + <% ScoringCriterium.grouped_by_category.each do |category, criteria| %> + <fieldset> + <legend><%= category.titlecase %></legend> + <% criteria.each do |criterium| %> + <% current_score = existing_scores.find { |score| score.scoring_criterium_id == criterium.id } %> + <p> + <%= select_tag "scores[#{criterium.id}][value]", + options_for_select(1..5, current_score&.value), + required: true, + prompt: "Score" %> + <%= label_tag "scores[#{criterium.id}][value]", + criterium.name.capitalize %> + </p> + <% end %> + </fieldset> + <% end %> + <%= f.submit submit_text %> +<% end %> diff --git a/app/views/scores/edit_all.html.erb b/app/views/scores/edit_all.html.erb new file mode 100644 index 0000000..2be7149 --- /dev/null +++ b/app/views/scores/edit_all.html.erb @@ -0,0 +1,7 @@ +<h1>Modifier les Notes pour la Tartiflette nº<%= @tartiflette.scoring_id %></h1> + +<%= render "form", + form_url: tartiflette_update_scores_path(@tartiflette), + form_method: :patch, + existing_scores: @scores, + submit_text: "Mettre à jour mes scores" %> diff --git a/app/views/scores/new.html.erb b/app/views/scores/new.html.erb new file mode 100644 index 0000000..cf171cc --- /dev/null +++ b/app/views/scores/new.html.erb @@ -0,0 +1,7 @@ +<h1>Noter la Tartiflette nº<%= @tartiflette.scoring_id %></h1> + +<%= render "form", + form_url: tartiflette_scores_path(@tartiflette), + form_method: :post, + existing_scores: [], + submit_text: "Envoyer mes scores" %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 0000000..51029b1 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,20 @@ +<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> +<%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %> + +<%= form_with url: session_path do |form| %> + <%= form.email_field :email_address, + required: true, + autofocus: true, + autocomplete: "username", + placeholder: "Enter your email address", + value: params[:email_address] %><br> + <%= form.password_field :password, + required: true, + autocomplete: "current-password", + placeholder: "Enter your password", + maxlength: 72 %><br> + <%= form.submit "Sign in" %> +<% end %> +<br> + +<%= link_to "Forgot password?", new_password_path %> diff --git a/app/views/tartiflettes/index.html.erb b/app/views/tartiflettes/index.html.erb new file mode 100644 index 0000000..09b2748 --- /dev/null +++ b/app/views/tartiflettes/index.html.erb @@ -0,0 +1,3 @@ +<h1>Tartiflettes</h1> + +<p>Toutes les tartiflettes</p> diff --git a/app/views/tartiflettes/show.html.erb b/app/views/tartiflettes/show.html.erb new file mode 100644 index 0000000..3753604 --- /dev/null +++ b/app/views/tartiflettes/show.html.erb @@ -0,0 +1,3 @@ +<h1>Tartiflette nº<%= @tartiflette.scoring_id %></h1> + +<p>Scores obtenus pour une tartiflette donnée.</p> |