diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 70c0386..64627e7 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -2,7 +2,7 @@ class AdminController < ApplicationController before_action :require_admin def index - @submissions = Submission.includes(:user).order(created_at: :desc) + @submissions = Submission.includes(:user, :votes).order(created_at: :desc) end def toggle_discard diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 51c1789..2d8011b 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,5 +1,10 @@ class PagesController < ApplicationController def home - @submissions = Submission.kept.order("RANDOM()") + @submissions = Submission.kept.includes(:votes).sort_by { |s| -s.votes.size } + if Current.user + @voted_submission_ids = Current.user.votes.where(submission_id: @submissions.map(&:id)).pluck(:submission_id).to_set + else + @voted_submission_ids = Set.new + end end end diff --git a/app/controllers/votes_controller.rb b/app/controllers/votes_controller.rb new file mode 100644 index 0000000..b713001 --- /dev/null +++ b/app/controllers/votes_controller.rb @@ -0,0 +1,26 @@ +class VotesController < ApplicationController + before_action :require_authentication + before_action :set_submission + + def create + vote = @submission.votes.new(user: Current.user) + + if vote.save + redirect_back fallback_location: root_path + else + redirect_back fallback_location: root_path, alert: vote.errors.full_messages.first + end + end + + def destroy + vote = @submission.votes.find_by(user: Current.user) + vote&.destroy + redirect_back fallback_location: root_path + end + + private + + def set_submission + @submission = Submission.find(params[:submission_id]) + end +end diff --git a/app/models/submission.rb b/app/models/submission.rb index 149beab..d507e7a 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -1,5 +1,6 @@ class Submission < ApplicationRecord belongs_to :user + has_many :votes validates :line1, :line2, :line3, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index 8fb17a2..032a7dc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,6 @@ class User < ApplicationRecord has_one :submission + has_many :votes def self.find_or_create_from_omniauth(auth) find_or_create_by(github_uid: auth.uid) do |user| diff --git a/app/models/vote.rb b/app/models/vote.rb new file mode 100644 index 0000000..d31cb3b --- /dev/null +++ b/app/models/vote.rb @@ -0,0 +1,15 @@ +class Vote < ApplicationRecord + belongs_to :user + belongs_to :submission + + validates :user_id, uniqueness: { scope: :submission_id } + validate :not_own_submission + + private + + def not_own_submission + if submission && user_id == submission.user_id + errors.add(:base, "You cannot vote on your own submission") + end + end +end diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb index 45c793d..438e03c 100644 --- a/app/views/admin/index.html.erb +++ b/app/views/admin/index.html.erb @@ -11,6 +11,7 @@ <%= submission.user.github_username %> (<%= submission.user.github_email %>) · <%= time_ago_in_words(submission.created_at) %> ago + · <%= submission.votes.size %> vote<%= submission.votes.size == 1 ? "" : "s" %> <% if submission.discarded? %> — discarded <% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 44402aa..1f9de27 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -13,9 +13,11 @@ <%= yield :head %> + - + + <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %> diff --git a/app/views/pages/home.html.erb b/app/views/pages/home.html.erb index f5dd016..c231be9 100644 --- a/app/views/pages/home.html.erb +++ b/app/views/pages/home.html.erb @@ -56,6 +56,27 @@ <%= submission.line2 %>
<%= submission.line3 %>

+ +
+
+ <%= submission.votes.size %> vote<%= submission.votes.size == 1 ? "" : "s" %> +
+ +
+ <% if Current.user && submission.user_id != Current.user.id %> + <% if @voted_submission_ids.include?(submission.id) %> + <%= button_to submission_vote_path(submission), method: :delete, class: "inline-flex items-center gap-1 text-sm px-3 py-1 rounded-full cursor-pointer bg-gray-400 bg-linear-to-t from-gray-500 to-gray-400 text-white" do %> + Voted + <% end %> + <% else %> + <%= button_to submission_vote_path(submission), method: :post, class: "inline-flex items-center gap-1 text-sm px-3 py-1 rounded-full cursor-pointer bg-[#C41C1C] bg-linear-to-t from-[#C41C1C] to-[#DD423E] text-white" do %> + ⬆ Vote + <% end %> + <% end %> + <% end %> +
+ +
<% end %> diff --git a/config/routes.rb b/config/routes.rb index 7c153bb..aeb1c5a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,7 +5,9 @@ get "auth/failure", to: "sessions#failure" delete "sign_out", to: "sessions#destroy" - resources :submissions, only: [ :new, :create, :update ] + resources :submissions, only: [ :new, :create, :update ] do + resource :vote, only: [ :create, :destroy ] + end get "admin", to: "admin#index" post "admin/:id/toggle_discard", to: "admin#toggle_discard", as: :admin_toggle_discard diff --git a/db/migrate/20260320191401_create_votes.rb b/db/migrate/20260320191401_create_votes.rb new file mode 100644 index 0000000..4a6a555 --- /dev/null +++ b/db/migrate/20260320191401_create_votes.rb @@ -0,0 +1,12 @@ +class CreateVotes < ActiveRecord::Migration[8.1] + def change + create_table :votes do |t| + t.references :user, null: false, foreign_key: true + t.references :submission, null: false, foreign_key: true + + t.timestamps + end + + add_index :votes, [ :user_id, :submission_id ], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 9db965d..50f3162 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.1].define(version: 2026_03_15_175159) do +ActiveRecord::Schema[8.1].define(version: 2026_03_20_191401) do create_table "submissions", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "discarded_at" @@ -33,5 +33,17 @@ t.index ["github_uid"], name: "index_users_on_github_uid", unique: true end + create_table "votes", force: :cascade do |t| + t.datetime "created_at", null: false + t.integer "submission_id", null: false + t.datetime "updated_at", null: false + t.integer "user_id", null: false + t.index ["submission_id"], name: "index_votes_on_submission_id" + t.index ["user_id", "submission_id"], name: "index_votes_on_user_id_and_submission_id", unique: true + t.index ["user_id"], name: "index_votes_on_user_id" + end + add_foreign_key "submissions", "users" + add_foreign_key "votes", "submissions" + add_foreign_key "votes", "users" end diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..dec3dd0 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..202cd3f Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..1a49c3c Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..704c12d Binary files /dev/null and b/public/icon-512.png differ diff --git a/public/icon.png b/public/icon.png index c4c9dbf..704c12d 100644 Binary files a/public/icon.png and b/public/icon.png differ diff --git a/public/icon.svg b/public/icon.svg index 04b34bf..de7ffc4 100644 --- a/public/icon.svg +++ b/public/icon.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..a893e5b --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,6 @@ +{ + "icons": [ + { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" }, + { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" } + ] +}