Adds types

This commit is contained in:
2026-03-29 02:37:49 -04:00
parent 63276ef8ca
commit 323484a33a
44 changed files with 1273 additions and 121 deletions

View File

@@ -1,4 +1,7 @@
# typed: true
class ApiTokensController < ApplicationController
extend T::Sig
before_action :require_authentication
def index

View File

@@ -1,20 +1,27 @@
# typed: true
class CollectionsController < ApplicationController
extend T::Sig
before_action :require_authentication
before_action :set_collection, only: [ :show, :edit, :update, :destroy, :games ]
sig { void }
def index
@root_collections = current_user.collections.root_collections.order(:name)
end
sig { void }
def show
@games = @collection.games.includes(:platform, :genres).page(params[:page]).per(25)
end
sig { void }
def new
@collection = current_user.collections.build
@collections = current_user.collections.root_collections.order(:name)
end
sig { void }
def create
@collection = current_user.collections.build(collection_params)
@@ -26,10 +33,12 @@ class CollectionsController < ApplicationController
end
end
sig { void }
def edit
@collections = current_user.collections.root_collections.where.not(id: @collection.id).order(:name)
end
sig { void }
def update
if @collection.update(collection_params)
redirect_to @collection, notice: "Collection was successfully updated."
@@ -39,11 +48,13 @@ class CollectionsController < ApplicationController
end
end
sig { void }
def destroy
@collection.destroy
redirect_to collections_path, notice: "Collection was successfully deleted."
end
sig { void }
def games
# Same as show, but maybe with different view
@games = @collection.games.includes(:platform, :genres).page(params[:page]).per(25)
@@ -52,10 +63,12 @@ class CollectionsController < ApplicationController
private
sig { void }
def set_collection
@collection = current_user.collections.find(params[:id])
end
sig { returns(T.untyped) }
def collection_params
permitted = params.require(:collection).permit(:name, :description, :parent_collection_id)
# Convert empty string to nil for parent_collection_id

View File

@@ -1,6 +1,10 @@
# typed: true
class DashboardController < ApplicationController
extend T::Sig
before_action :require_authentication
sig { void }
def index
@recently_added_games = current_user.games.recent.limit(5)
@currently_playing_games = current_user.games.currently_playing.limit(5)
@@ -10,17 +14,7 @@ class DashboardController < ApplicationController
@completed_games = current_user.games.completed.count
@backlog_games = current_user.games.backlog.count
@total_spent = current_user.games.sum(:price_paid) || 0
@games_by_platform = current_user.games.joins(:platform)
.group("platforms.name")
.count
.sort_by { |_, count| -count }
.first(5)
@games_by_genre = current_user.games.joins(:genres)
.group("genres.name")
.count
.sort_by { |_, count| -count }
.first(5)
end
end

View File

@@ -1,7 +1,12 @@
# typed: true
class GamesController < ApplicationController
extend T::Sig
before_action :require_authentication
before_action :set_game, only: [ :show, :edit, :update, :destroy ]
sig { void }
def index
@games = current_user.games.includes(:platform, :genres, :collections)
@@ -27,9 +32,11 @@ class GamesController < ApplicationController
@genres = Genre.order(:name)
end
sig { void }
def show
end
sig { void }
def new
@game = current_user.games.build
@platforms = Platform.order(:name)
@@ -37,13 +44,14 @@ class GamesController < ApplicationController
@collections = current_user.collections.order(:name)
end
sig { void }
def create
@game = current_user.games.build(game_params)
if @game.save
# If game was created with IGDB ID, sync the metadata
sync_igdb_metadata_after_create if @game.igdb_id.present?
redirect_to @game, notice: "Game was successfully created."
else
@platforms = Platform.order(:name)
@@ -53,12 +61,14 @@ class GamesController < ApplicationController
end
end
sig { void }
def edit
@platforms = Platform.order(:name)
@genres = Genre.order(:name)
@collections = current_user.collections.order(:name)
end
sig { void }
def update
if @game.update(game_params)
redirect_to @game, notice: "Game was successfully updated."
@@ -70,50 +80,54 @@ class GamesController < ApplicationController
end
end
sig { void }
def destroy
@game.destroy
redirect_to games_path, notice: "Game was successfully deleted."
end
sig { void }
def import
@platforms = Platform.order(:name)
@genres = Genre.order(:name)
@collections = current_user.collections.order(:name)
end
sig { void }
def bulk_edit
@game_ids = params[:game_ids] || []
if @game_ids.empty?
redirect_to games_path, alert: "Please select at least one game to edit."
return
end
@games = current_user.games.where(id: @game_ids)
@platforms = Platform.order(:name)
@genres = Genre.order(:name)
@collections = current_user.collections.order(:name)
end
sig { void }
def bulk_update
@game_ids = params[:game_ids] || []
if @game_ids.empty?
redirect_to games_path, alert: "No games selected."
return
end
@games = current_user.games.where(id: @game_ids)
updated_count = 0
@games.each do |game|
updates = {}
# Only update fields that have values provided
updates[:completion_status] = params[:completion_status] if params[:completion_status].present?
updates[:location] = params[:location] if params[:location].present?
updates[:condition] = params[:condition] if params[:condition].present?
# Handle collection assignment
if params[:collection_action].present?
case params[:collection_action]
@@ -129,7 +143,7 @@ class GamesController < ApplicationController
game.collection_ids = params[:collection_ids] if params[:collection_ids].present?
end
end
# Handle genre assignment
if params[:genre_action].present?
case params[:genre_action]
@@ -145,15 +159,16 @@ class GamesController < ApplicationController
game.genre_ids = params[:genre_ids] if params[:genre_ids].present?
end
end
if game.update(updates)
updated_count += 1
end
end
redirect_to games_path, notice: "Successfully updated #{updated_count} game(s)."
end
sig { void }
def bulk_create
require "csv"
@@ -163,9 +178,11 @@ class GamesController < ApplicationController
csv_text = params[:csv_file].read
csv = CSV.parse(csv_text, headers: true)
csv.each_with_index do |row, index|
csv.each_with_index do |csv_row, index|
# Cast to untyped to avoid Sorbet CSV::Row vs Hash confusion
row = T.let(csv_row, T.untyped)
platform = Platform.find_by(name: row["platform"]) || Platform.find_by(abbreviation: row["platform"])
unless platform
results[:failed] += 1
results[:errors] << "Row #{index + 2}: Platform '#{row['platform']}' not found"
@@ -209,10 +226,11 @@ class GamesController < ApplicationController
end
end
sig { void }
def search_igdb
query = params[:q].to_s.strip
platform_id = params[:platform_id]
if query.length < 2
render json: []
return
@@ -221,15 +239,15 @@ class GamesController < ApplicationController
begin
service = IgdbService.new
platform = platform_id.present? ? Platform.find_by(id: platform_id) : nil
# Search IGDB (limit to 10 results for autocomplete)
results = service.search_game(query, platform, 10)
# Format results for autocomplete
formatted_results = results.map do |result|
# Map IGDB genres to our local genre IDs
genre_ids = map_igdb_genres_to_ids(result[:genres] || [])
{
igdb_id: result[:igdb_id],
name: result[:name],
@@ -242,7 +260,7 @@ class GamesController < ApplicationController
confidence: result[:confidence_score]
}
end
render json: formatted_results
rescue => e
Rails.logger.error("IGDB search error: #{e.message}")
@@ -250,49 +268,53 @@ class GamesController < ApplicationController
end
end
sig { void }
def search_locations
query = params[:q].to_s.strip
# Get unique locations from user's games that match the query
locations = current_user.games
.where.not(location: [nil, ""])
.where.not(location: [ nil, "" ])
.where("location ILIKE ?", "%#{query}%")
.select(:location)
.distinct
.order(:location)
.limit(10)
.pluck(:location)
render json: locations
end
sig { void }
def search_stores
query = params[:q].to_s.strip
# Get unique digital stores from user's games that match the query
stores = current_user.games
.where.not(digital_store: [nil, ""])
.where.not(digital_store: [ nil, "" ])
.where("digital_store ILIKE ?", "%#{query}%")
.select(:digital_store)
.distinct
.order(:digital_store)
.limit(10)
.pluck(:digital_store)
render json: stores
end
private
sig { void }
def set_game
@game = current_user.games.includes(:igdb_game).find(params[:id])
end
sig { void }
def sync_igdb_metadata_after_create
# Fetch full game data from IGDB
service = IgdbService.new
igdb_data = service.get_game(@game.igdb_id)
return unless igdb_data
# Create or update IgdbGame record
@@ -301,11 +323,11 @@ class GamesController < ApplicationController
ig.slug = igdb_data["slug"]
ig.summary = igdb_data["summary"]
ig.first_release_date = igdb_data["first_release_date"] ? Time.at(igdb_data["first_release_date"]).to_date : nil
# Extract cover URL
cover_url = igdb_data.dig("cover", "url")&.split("/")&.last&.sub(".jpg", "")
ig.cover_url = cover_url
ig.last_synced_at = Time.current
end
@@ -327,9 +349,10 @@ class GamesController < ApplicationController
Rails.logger.error("Failed to sync IGDB metadata: #{e.message}")
end
sig { params(genre_names: T::Array[String]).returns(T::Array[Integer]) }
def map_igdb_genres_to_ids(genre_names)
return [] if genre_names.blank?
# Genre mapping (same as in IgdbMatchSuggestion)
genre_mappings = {
"Role-playing (RPG)" => "RPG",
@@ -352,28 +375,30 @@ class GamesController < ApplicationController
genre_names.each do |igdb_genre_name|
# Try exact match
local_genre = Genre.find_by("LOWER(name) = ?", igdb_genre_name.downcase)
# Try mapped name
if local_genre.nil? && genre_mappings[igdb_genre_name]
mapped_name = genre_mappings[igdb_genre_name]
local_genre = Genre.find_by("LOWER(name) = ?", mapped_name.downcase)
end
genre_ids << local_genre.id if local_genre
end
genre_ids
end
sig { params(genre_names: T::Array[String]).void }
def assign_igdb_genres_to_game(genre_names)
genre_ids = map_igdb_genres_to_ids(genre_names)
genre_ids.each do |genre_id|
genre = Genre.find(genre_id)
@game.genres << genre unless @game.genres.include?(genre)
end
end
sig { returns(T.untyped) }
def game_params
params.require(:game).permit(
:title, :platform_id, :format, :date_added, :completion_status,

View File

@@ -1,4 +1,7 @@
# typed: true
class IgdbMatchesController < ApplicationController
extend T::Sig
before_action :require_authentication
before_action :set_suggestion, only: [ :approve, :reject ]
@@ -10,7 +13,7 @@ class IgdbMatchesController < ApplicationController
.where(games: { igdb_id: nil })
.includes(game: :platform)
.group_by(&:game)
@matched_games = current_user.games.igdb_matched.count
@unmatched_games = current_user.games.igdb_unmatched.count
@pending_review_count = current_user.igdb_match_suggestions
@@ -43,8 +46,8 @@ class IgdbMatchesController < ApplicationController
end
# Check if games need syncing
unmatched_count = current_user.games.igdb_unmatched.where(igdb_match_status: [nil, "failed"]).count
unmatched_count = current_user.games.igdb_unmatched.where(igdb_match_status: [ nil, "failed" ]).count
if unmatched_count == 0
redirect_to igdb_matches_path, alert: "All games are already matched or being processed!"
return
@@ -53,7 +56,7 @@ class IgdbMatchesController < ApplicationController
# Try to run job immediately for faster feedback
begin
IgdbSyncJob.perform_later
redirect_to igdb_matches_path, notice: "IGDB sync started! Processing #{unmatched_count} games. This may take a few minutes - the page will auto-refresh."
rescue => e
Rails.logger.error("Failed to start IGDB sync: #{e.message}")

View File

@@ -1,4 +1,7 @@
# typed: true
class PagesController < ApplicationController
extend T::Sig
def home
if user_signed_in?
redirect_to dashboard_path

View File

@@ -1,4 +1,7 @@
# typed: true
class PasswordResetsController < ApplicationController
extend T::Sig
before_action :require_no_authentication, only: [ :new, :create, :edit, :update ]
before_action :set_user_by_token, only: [ :edit, :update ]

View File

@@ -1,4 +1,7 @@
# typed: true
class ProfilesController < ApplicationController
extend T::Sig
def show
@user = User.find_by!(username: params[:username])
@@ -11,13 +14,13 @@ class ProfilesController < ApplicationController
@physical_games = @user.games.physical_games.count
@digital_games = @user.games.digital_games.count
@completed_games = @user.games.completed.count
@games_by_platform = @user.games.joins(:platform)
.group("platforms.name")
.count
.sort_by { |_, count| -count }
.first(10)
@public_collections = @user.collections.root_collections.order(:name)
@recent_games = @user.games.includes(:platform).recent.limit(10)
end

View File

@@ -1,10 +1,16 @@
# typed: true
class SessionsController < ApplicationController
extend T::Sig
before_action :require_no_authentication, only: [ :new, :create ]
before_action :require_authentication, only: [ :destroy ]
sig { void }
def new
end
sig { void }
def create
user = User.find_by(email: params[:email].downcase)
@@ -17,6 +23,7 @@ class SessionsController < ApplicationController
end
end
sig { void }
def destroy
sign_out
redirect_to root_path, notice: "You have been signed out."

View File

@@ -1,12 +1,17 @@
# typed: true
class UsersController < ApplicationController
extend T::Sig
before_action :require_no_authentication, only: [ :new, :create ]
before_action :require_authentication, only: [ :edit, :update, :settings ]
before_action :set_user, only: [ :edit, :update ]
sig { void }
def new
@user = User.new
end
sig { void }
def create
@user = User.new(user_params)
@@ -18,9 +23,11 @@ class UsersController < ApplicationController
end
end
sig { void }
def edit
end
sig { void }
def update
if @user.update(user_params)
redirect_to settings_path, notice: "Your profile has been updated."
@@ -29,6 +36,7 @@ class UsersController < ApplicationController
end
end
sig { void }
def settings
@user = current_user
@api_tokens = current_user.api_tokens.order(created_at: :desc)
@@ -36,10 +44,12 @@ class UsersController < ApplicationController
private
sig { void }
def set_user
@user = current_user
end
sig { returns(T.untyped) }
def user_params
params.require(:user).permit(:email, :username, :password, :password_confirmation, :bio, :profile_public, :igdb_sync_enabled, :theme)
end