Moving to github

This commit is contained in:
2026-03-28 19:24:29 -04:00
commit 036fa7ab33
302 changed files with 17838 additions and 0 deletions

View File

@@ -0,0 +1,259 @@
<% igdb_enabled = game.new_record? && current_user.igdb_sync_enabled? %>
<% if igdb_enabled %>
<div data-controller="igdb-search"
data-igdb-search-url-value="<%= search_igdb_games_path %>"
data-action="click@window->igdb-search#clickOutside">
<% else %>
<div>
<% end %>
<%= form_with model: game, class: "space-y-6" do |f| %>
<% if game.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<ul>
<% game.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<% if igdb_enabled %>
<!-- IGDB Search Section (only for new games) -->
<div class="bg-indigo-50 border-l-4 border-indigo-500 p-4 mb-6">
<h3 class="text-lg font-semibold text-indigo-900 mb-3">🔍 Search IGDB Database</h3>
<p class="text-sm text-indigo-700 mb-4">
Start typing to search the IGDB game database. Select a match to auto-fill details, or add a custom game manually.
</p>
<div class="relative">
<div class="relative">
<input type="text"
placeholder="Search for a game (e.g., 'Zelda Ocarina', 'Mario 64')..."
class="w-full px-4 py-3 pr-10 rounded-lg border-2 border-indigo-300 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200"
data-igdb-search-target="query"
data-action="input->igdb-search#search"
autocomplete="off">
<button type="button"
class="absolute right-2 top-2 p-2 text-gray-400 hover:text-gray-600"
data-action="click->igdb-search#clearSearch"
title="Clear search">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<!-- Results dropdown -->
<div class="hidden absolute z-10 w-full mt-1 bg-white rounded-lg shadow-lg border border-gray-200 max-h-96 overflow-y-auto"
data-igdb-search-target="results">
</div>
</div>
<!-- Hidden field for IGDB ID -->
<%= f.hidden_field :igdb_id, "data-igdb-search-target": "igdbId" %>
</div>
<% end %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<%= f.label :title, class: "block text-sm font-medium text-gray-700" %>
<% if game.new_record? && current_user.igdb_sync_enabled? %>
<%= f.text_field :title,
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500",
"data-igdb-search-target": "title" %>
<p class="mt-1 text-xs text-gray-500">Auto-filled from IGDB or enter manually</p>
<% else %>
<%= f.text_field :title,
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
<% end %>
</div>
<div>
<%= f.label :platform_id, class: "block text-sm font-medium text-gray-700" %>
<% if game.new_record? && current_user.igdb_sync_enabled? %>
<%= f.collection_select :platform_id, @platforms, :id, :name,
{ prompt: "Select Platform" },
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500",
"data-igdb-search-target": "platformSelect",
"data-action": "change->igdb-search#search" %>
<% else %>
<%= f.collection_select :platform_id, @platforms, :id, :name,
{ prompt: "Select Platform" },
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
<% end %>
</div>
<div>
<%= f.label :format, class: "block text-sm font-medium text-gray-700" %>
<%= f.select :format, [["Physical", "physical"], ["Digital", "digital"]], { prompt: "Select Format" }, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div>
<%= f.label :date_added, class: "block text-sm font-medium text-gray-700" %>
<%= f.date_field :date_added, value: game.date_added || Date.current, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div>
<%= f.label :completion_status, class: "block text-sm font-medium text-gray-700" %>
<%= f.select :completion_status, [["Backlog", "backlog"], ["Currently Playing", "currently_playing"], ["Completed", "completed"], ["On Hold", "on_hold"], ["Not Playing", "not_playing"]], { include_blank: "Select Status" }, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div>
<%= f.label :user_rating, "Rating (1-5 stars)", class: "block text-sm font-medium text-gray-700" %>
<%= f.number_field :user_rating, min: 1, max: 5, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
</div>
<div>
<%= f.label :genre_ids, "Genres", class: "block text-sm font-medium text-gray-700" %>
<%= f.collection_check_boxes :genre_ids, @genres, :id, :name do |b| %>
<div class="inline-block mr-4">
<%= b.check_box class: "rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" %>
<%= b.label class: "ml-2 text-sm text-gray-700" %>
</div>
<% end %>
</div>
<div id="physical-fields" style="<%= 'display: none;' if game.digital? %>">
<h3 class="text-lg font-medium text-gray-900 mb-4">Physical Game Details</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<%= f.label :condition, class: "block text-sm font-medium text-gray-700" %>
<%= f.select :condition, [["CIB (Complete in Box)", "cib"], ["Loose", "loose"], ["Sealed", "sealed"], ["Good", "good"], ["Fair", "fair"]], { include_blank: "Select Condition" }, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div>
<%= f.label :price_paid, class: "block text-sm font-medium text-gray-700" %>
<%= f.number_field :price_paid, step: 0.01, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div data-controller="location-autocomplete"
data-location-autocomplete-url-value="<%= search_locations_games_path %>"
data-action="click@window->location-autocomplete#clickOutside">
<%= f.label :location, class: "block text-sm font-medium text-gray-700" %>
<div class="relative">
<%= f.text_field :location,
placeholder: "e.g., Bedroom Shelf A",
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500",
autocomplete: "off",
data: {
location_autocomplete_target: "input",
action: "input->location-autocomplete#search"
} %>
<!-- Autocomplete results dropdown -->
<div class="hidden absolute z-20 w-full mt-1 bg-white rounded-md shadow-lg border border-gray-200 max-h-48 overflow-y-auto"
data-location-autocomplete-target="results">
</div>
</div>
<p class="mt-1 text-xs text-gray-500">Start typing to see previously used locations</p>
</div>
</div>
</div>
<div id="digital-fields" style="<%= 'display: none;' if game.physical? %>">
<h3 class="text-lg font-medium text-gray-900 mb-4">Digital Game Details</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div data-controller="location-autocomplete"
data-location-autocomplete-url-value="<%= search_stores_games_path %>"
data-action="click@window->location-autocomplete#clickOutside">
<%= f.label :digital_store, "Digital Store/Platform", class: "block text-sm font-medium text-gray-700" %>
<div class="relative">
<%= f.text_field :digital_store,
placeholder: "e.g., Steam, PlayStation Store",
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500",
autocomplete: "off",
data: {
location_autocomplete_target: "input",
action: "input->location-autocomplete#search"
} %>
<!-- Autocomplete results dropdown -->
<div class="hidden absolute z-20 w-full mt-1 bg-white rounded-md shadow-lg border border-gray-200 max-h-48 overflow-y-auto"
data-location-autocomplete-target="results">
</div>
</div>
<p class="mt-1 text-xs text-gray-500">Start typing to see previously used stores</p>
</div>
<div>
<%= f.label :price_paid, class: "block text-sm font-medium text-gray-700" %>
<%= f.number_field :price_paid, step: 0.01, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
</div>
</div>
<div>
<%= f.label :collection_ids, "Collections", class: "block text-sm font-medium text-gray-700 mb-2" %>
<% if defined?(@collections) && @collections.any? %>
<div class="space-y-2 max-h-48 overflow-y-auto border border-gray-300 rounded-md p-3 bg-gray-50">
<% @collections.each do |collection| %>
<div class="flex items-start">
<%= check_box_tag "game[collection_ids][]", collection.id,
game.collection_ids.include?(collection.id),
id: "game_collection_ids_#{collection.id}",
class: "rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 mt-1" %>
<%= label_tag "game_collection_ids_#{collection.id}", class: "ml-2 text-sm" do %>
<span class="font-medium text-gray-900"><%= collection.name %></span>
<% if collection.subcollection? %>
<span class="text-gray-500 text-xs">(subcollection of <%= collection.parent_collection.name %>)</span>
<% end %>
<% end %>
</div>
<% end %>
</div>
<p class="mt-1 text-sm text-gray-500">Select one or more collections for this game</p>
<% else %>
<p class="text-gray-500 text-sm">
No collections yet. <%= link_to "Create a collection", new_collection_path, class: "text-indigo-600 hover:text-indigo-800" %> to organize your games.
</p>
<% end %>
</div>
<div>
<%= f.label :notes, class: "block text-sm font-medium text-gray-700" %>
<% if game.new_record? && current_user.igdb_sync_enabled? %>
<%= f.text_area :notes,
rows: 4,
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500",
placeholder: "Optional notes about this game (can be auto-filled from IGDB)",
"data-igdb-search-target": "summary" %>
<p class="mt-1 text-xs text-gray-500">Can be auto-filled from IGDB or add your own notes</p>
<% else %>
<%= f.text_area :notes,
rows: 4,
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500",
placeholder: "Optional notes about this game" %>
<% end %>
</div>
<div class="flex justify-between">
<%= f.submit class: "px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700" %>
<%= link_to "Cancel", game.persisted? ? game : games_path, class: "px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300" %>
</div>
<% end %>
</div>
<script>
document.addEventListener('turbo:load', function() {
const formatField = document.querySelector('#game_format');
const physicalFields = document.querySelector('#physical-fields');
const digitalFields = document.querySelector('#digital-fields');
if (formatField) {
formatField.addEventListener('change', function() {
if (this.value === 'physical') {
physicalFields.style.display = 'block';
digitalFields.style.display = 'none';
} else if (this.value === 'digital') {
physicalFields.style.display = 'none';
digitalFields.style.display = 'block';
}
});
}
});
</script>

View File

@@ -0,0 +1,4 @@
<div>
<h1 class="font-bold text-4xl">Games#bulk_create</h1>
<p>Find me in app/views/games/bulk_create.html.erb</p>
</div>

View File

@@ -0,0 +1,148 @@
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Bulk Edit <%= pluralize(@games.count, "Game") %></h1>
<div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700">
<strong>Tip:</strong> Only fill in the fields you want to update. Empty fields will be left unchanged.
</p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow mb-6">
<h2 class="text-xl font-bold mb-4">Selected Games</h2>
<div class="space-y-1">
<% @games.each do |game| %>
<div class="text-sm text-gray-700">• <%= game.title %> (<%= game.platform.name %>)</div>
<% end %>
</div>
</div>
<%= form_with url: bulk_update_games_path, method: :patch, class: "bg-white p-6 rounded-lg shadow space-y-6" do |f| %>
<% @game_ids.each do |id| %>
<%= hidden_field_tag "game_ids[]", id %>
<% end %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<%= label_tag :completion_status, "Completion Status", class: "block text-sm font-medium text-gray-700" %>
<%= select_tag :completion_status,
options_for_select([
["Don't Change", ""],
["Backlog", "backlog"],
["Currently Playing", "currently_playing"],
["Completed", "completed"],
["On Hold", "on_hold"],
["Not Playing", "not_playing"]
]),
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div>
<%= label_tag :condition, "Condition (Physical Games)", class: "block text-sm font-medium text-gray-700" %>
<%= select_tag :condition,
options_for_select([
["Don't Change", ""],
["CIB (Complete in Box)", "cib"],
["Loose", "loose"],
["Sealed", "sealed"],
["Good", "good"],
["Fair", "fair"]
]),
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div class="md:col-span-2">
<%= label_tag :location, "Location", class: "block text-sm font-medium text-gray-700" %>
<%= text_field_tag :location, "",
placeholder: "Leave empty to not change",
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
<p class="mt-1 text-sm text-gray-500">Will update all selected games to this location</p>
</div>
</div>
<div class="border-t pt-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Collections</h3>
<div class="mb-4">
<%= label_tag :collection_action, "Collection Action", class: "block text-sm font-medium text-gray-700" %>
<%= select_tag :collection_action,
options_for_select([
["Don't Change", ""],
["Add to Collections", "add"],
["Remove from Collections", "remove"],
["Replace Collections", "replace"]
]),
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
<p class="mt-1 text-sm text-gray-500">
<strong>Add:</strong> Adds to existing collections |
<strong>Remove:</strong> Removes from selected collections |
<strong>Replace:</strong> Sets to only selected collections
</p>
</div>
<% if @collections.any? %>
<div class="space-y-2 max-h-48 overflow-y-auto border border-gray-300 rounded-md p-3 bg-gray-50">
<% @collections.each do |collection| %>
<div class="flex items-start">
<%= check_box_tag "collection_ids[]", collection.id, false,
id: "collection_ids_#{collection.id}",
class: "rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 mt-1" %>
<%= label_tag "collection_ids_#{collection.id}", class: "ml-2 text-sm" do %>
<span class="font-medium text-gray-900"><%= collection.name %></span>
<% if collection.subcollection? %>
<span class="text-gray-500 text-xs">(subcollection of <%= collection.parent_collection.name %>)</span>
<% end %>
<% end %>
</div>
<% end %>
</div>
<% else %>
<p class="text-gray-500 text-sm">
No collections yet. <%= link_to "Create a collection", new_collection_path, class: "text-indigo-600 hover:text-indigo-800" %> first.
</p>
<% end %>
</div>
<div class="border-t pt-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Genres</h3>
<div class="mb-4">
<%= label_tag :genre_action, "Genre Action", class: "block text-sm font-medium text-gray-700" %>
<%= select_tag :genre_action,
options_for_select([
["Don't Change", ""],
["Add Genres", "add"],
["Remove Genres", "remove"],
["Replace Genres", "replace"]
]),
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div class="space-y-2 max-h-48 overflow-y-auto border border-gray-300 rounded-md p-3 bg-gray-50">
<% @genres.each do |genre| %>
<div class="inline-block mr-4">
<%= check_box_tag "genre_ids[]", genre.id, false,
id: "genre_ids_#{genre.id}",
class: "rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" %>
<%= label_tag "genre_ids_#{genre.id}", genre.name, class: "ml-2 text-sm text-gray-700" %>
</div>
<% end %>
</div>
</div>
<div class="flex justify-between pt-6 border-t">
<%= submit_tag "Update #{pluralize(@games.count, 'Game')}",
class: "px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700",
data: { confirm: "Are you sure you want to update #{@games.count} game(s)?" } %>
<%= link_to "Cancel", games_path, class: "px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300" %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,4 @@
<div>
<h1 class="font-bold text-4xl">Games#create</h1>
<p>Find me in app/views/games/create.html.erb</p>
</div>

View File

@@ -0,0 +1,4 @@
<div>
<h1 class="font-bold text-4xl">Games#destroy</h1>
<p>Find me in app/views/games/destroy.html.erb</p>
</div>

View File

@@ -0,0 +1,7 @@
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Edit Game</h1>
<div class="bg-white p-6 rounded-lg shadow">
<%= render "form", game: @game %>
</div>
</div>

View File

@@ -0,0 +1,50 @@
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Import Games from CSV</h1>
<div class="bg-white p-6 rounded-lg shadow mb-6">
<h2 class="text-xl font-bold mb-4">CSV Format</h2>
<p class="text-gray-600 mb-4">Your CSV file should have the following columns:</p>
<div class="bg-gray-100 p-4 rounded mb-4 overflow-x-auto">
<code class="text-sm">
title,platform,format,genres,completion_status,user_rating,condition,price_paid,location,digital_store,date_added,notes
</code>
</div>
<div class="space-y-2 text-sm text-gray-600">
<p><strong>Required fields:</strong> title, platform, format</p>
<p><strong>Platform:</strong> Use the full name or abbreviation (e.g., "Nintendo 64" or "N64")</p>
<p><strong>Format:</strong> Either "physical" or "digital"</p>
<p><strong>Genres:</strong> Separate multiple genres with | (e.g., "Action|Adventure")</p>
<p><strong>Completion Status:</strong> backlog, currently_playing, completed, on_hold, not_playing</p>
<p><strong>User Rating:</strong> Number from 1 to 5</p>
<p><strong>Condition (physical only):</strong> cib, loose, sealed, good, fair</p>
<p><strong>Date Added:</strong> YYYY-MM-DD format (defaults to today if not provided)</p>
</div>
<div class="mt-6">
<h3 class="font-bold mb-2">Example CSV:</h3>
<div class="bg-gray-100 p-4 rounded overflow-x-auto">
<pre class="text-xs">title,platform,format,genres,completion_status,user_rating,condition,price_paid,location,digital_store,date_added,notes
The Legend of Zelda: Ocarina of Time,N64,physical,Action|Adventure,completed,5,cib,45.00,Shelf A,,2024-01-15,One of the best games ever
Elden Ring,PS5,digital,Action|RPG,currently_playing,5,,,PlayStation Store,2024-03-01,Amazing open world</pre>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Upload CSV File</h2>
<%= form_with url: bulk_create_games_path, multipart: true, class: "space-y-4" do |f| %>
<div>
<%= f.label :csv_file, "Select CSV File", class: "block text-sm font-medium text-gray-700 mb-2" %>
<%= f.file_field :csv_file, accept: ".csv", required: true, class: "block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100" %>
</div>
<div class="flex space-x-4">
<%= f.submit "Import Games", class: "px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700" %>
<%= link_to "Cancel", games_path, class: "px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300" %>
</div>
<% end %>
</div>
</div>

View File

@@ -0,0 +1,155 @@
<div>
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">My Games</h1>
<div class="flex items-center space-x-2">
<div id="bulk-actions" style="display: none;" class="mr-4">
<button type="button" onclick="bulkEdit()" class="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700">
Bulk Edit (<span id="selected-count">0</span>)
</button>
</div>
<%= link_to "Import CSV", import_games_path, class: "px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700" %>
<%= link_to "Add Game", new_game_path, class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
</div>
</div>
<!-- Filters -->
<%= form_with url: games_path, method: :get, class: "bg-white p-4 rounded-lg shadow mb-6" do |f| %>
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<div>
<%= f.text_field :search, placeholder: "Search games...", value: params[:search], class: "w-full rounded-md border-gray-300" %>
</div>
<div>
<%= f.select :platform_id, options_from_collection_for_select(@platforms, :id, :name, params[:platform_id]), { include_blank: "All Platforms" }, class: "w-full rounded-md border-gray-300" %>
</div>
<div>
<%= f.select :genre_id, options_from_collection_for_select(@genres, :id, :name, params[:genre_id]), { include_blank: "All Genres" }, class: "w-full rounded-md border-gray-300" %>
</div>
<div>
<%= f.select :format, options_for_select([["Physical", "physical"], ["Digital", "digital"]], params[:format]), { include_blank: "All Formats" }, class: "w-full rounded-md border-gray-300" %>
</div>
<div>
<%= f.select :completion_status, options_for_select([["Backlog", "backlog"], ["Currently Playing", "currently_playing"], ["Completed", "completed"], ["On Hold", "on_hold"], ["Not Playing", "not_playing"]], params[:completion_status]), { include_blank: "All Statuses" }, class: "w-full rounded-md border-gray-300" %>
</div>
</div>
<div class="mt-4 flex justify-between">
<%= f.submit "Filter", class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
<%= link_to "Clear Filters", games_path, class: "px-4 py-2 bg-gray-200 rounded hover:bg-gray-300" %>
</div>
<% end %>
<!-- Sort -->
<div class="mb-4">
<span class="text-gray-600 mr-2">Sort by:</span>
<%= link_to "Alphabetical", games_path(filter_params_for_sort("alphabetical")), class: "text-indigo-600 hover:text-indigo-800 mr-4" %>
<%= link_to "Recently Added", games_path(filter_params_for_sort("recent")), class: "text-indigo-600 hover:text-indigo-800 mr-4" %>
<%= link_to "Highest Rated", games_path(filter_params_for_sort("rated")), class: "text-indigo-600 hover:text-indigo-800" %>
</div>
<!-- Games List -->
<% if @games.any? %>
<div class="bg-white rounded-lg shadow overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left">
<input type="checkbox" id="select-all" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" onchange="toggleAll(this)">
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Platform</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Format</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rating</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<% @games.each do |game| %>
<tr>
<td class="px-6 py-4">
<input type="checkbox" name="game_ids[]" value="<%= game.id %>" class="game-checkbox rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" onchange="updateBulkActions()">
</td>
<td class="px-6 py-4 whitespace-nowrap">
<%= link_to game.title, game, class: "text-indigo-600 hover:text-indigo-800 font-medium" %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<%= game.platform.name %>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full <%= game.physical? ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' %>">
<%= game.format.titleize %>
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<%= game.completion_status&.titleize || "N/A" %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<%= game.user_rating ? "⭐ #{game.user_rating}/5" : "Not rated" %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<%= link_to "View", game, class: "text-indigo-600 hover:text-indigo-900 mr-2" %>
<%= link_to "Edit", edit_game_path(game), class: "text-blue-600 hover:text-blue-900 mr-2" %>
<%= button_to "Delete", game, method: :delete, data: { turbo_confirm: "Are you sure you want to delete '#{game.title}'? This action cannot be undone." }, class: "text-red-600 hover:text-red-900" %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<!-- Pagination would go here -->
<div class="mt-4">
<%#= paginate @games %>
</div>
<% else %>
<div class="bg-white p-8 rounded-lg shadow text-center">
<p class="text-gray-500 mb-4">No games found matching your filters.</p>
<%= link_to "Clear Filters", games_path, class: "text-indigo-600 hover:text-indigo-800" %>
</div>
<% end %>
</div>
<script>
function toggleAll(checkbox) {
const gameCheckboxes = document.querySelectorAll('.game-checkbox');
gameCheckboxes.forEach(cb => {
cb.checked = checkbox.checked;
});
updateBulkActions();
}
function updateBulkActions() {
const checkedBoxes = document.querySelectorAll('.game-checkbox:checked');
const count = checkedBoxes.length;
const bulkActions = document.getElementById('bulk-actions');
const selectedCount = document.getElementById('selected-count');
if (count > 0) {
bulkActions.style.display = 'block';
selectedCount.textContent = count;
} else {
bulkActions.style.display = 'none';
document.getElementById('select-all').checked = false;
}
}
function bulkEdit() {
const checkedBoxes = document.querySelectorAll('.game-checkbox:checked');
const gameIds = Array.from(checkedBoxes).map(cb => cb.value);
if (gameIds.length === 0) {
alert('Please select at least one game');
return;
}
// Build URL with game IDs
const params = new URLSearchParams();
gameIds.forEach(id => params.append('game_ids[]', id));
window.location.href = '<%= bulk_edit_games_path %>?' + params.toString();
}
</script>

View File

@@ -0,0 +1,7 @@
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Add New Game</h1>
<div class="bg-white p-6 rounded-lg shadow">
<%= render "form", game: @game %>
</div>
</div>

View File

@@ -0,0 +1,169 @@
<div class="max-w-4xl mx-auto">
<div class="bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-start mb-6">
<div class="flex gap-6">
<!-- IGDB Cover Image -->
<% if @game.igdb_game&.cover_url.present? %>
<div class="flex-shrink-0">
<%= image_tag @game.igdb_game.cover_image_url("cover_big"),
alt: @game.title,
class: "w-32 h-44 object-cover rounded-lg shadow-lg" %>
<div class="mt-2 text-center">
<span class="text-xs text-gray-500 flex items-center justify-center gap-1">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z"/>
</svg>
IGDB
</span>
</div>
</div>
<% end %>
<!-- Title and Platform -->
<div>
<h1 class="text-3xl font-bold mb-2"><%= @game.title %></h1>
<p class="text-gray-600 mb-2"><%= @game.platform.name %></p>
<!-- IGDB Match Status -->
<% if @game.igdb_id.present? %>
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs font-semibold rounded">
✓ IGDB Matched
</span>
<% if @game.igdb_match_confidence %>
<span class="text-xs text-gray-500">
<%= @game.igdb_match_confidence.to_i %>% confidence
</span>
<% end %>
</div>
<!-- IGDB Release Date -->
<% if @game.igdb_game&.first_release_date %>
<p class="text-sm text-gray-600">
<strong>Released:</strong> <%= @game.igdb_game.first_release_date.strftime("%B %Y") %>
</p>
<% end %>
<% elsif current_user.igdb_sync_enabled? %>
<div class="flex items-center gap-2">
<span class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded">
No IGDB Match
</span>
<%= link_to "Find Match", igdb_matches_path, class: "text-xs text-indigo-600 hover:text-indigo-800" %>
</div>
<% end %>
</div>
</div>
<div class="flex space-x-2">
<%= link_to "Edit", edit_game_path(@game), class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
<%= button_to "Delete", @game, method: :delete, data: { turbo_confirm: "Are you sure you want to delete '#{@game.title}'? This will permanently remove it from your collection." }, class: "px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700" %>
</div>
</div>
<!-- IGDB Summary -->
<% if @game.igdb_game&.summary.present? %>
<div class="mb-6 p-4 bg-gray-50 rounded-lg border-l-4 border-indigo-500">
<h3 class="text-sm font-semibold text-gray-700 mb-2">About this game (from IGDB)</h3>
<p class="text-gray-700 text-sm leading-relaxed"><%= @game.igdb_game.summary %></p>
</div>
<% end %>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Format</label>
<span class="px-3 py-1 inline-flex text-sm font-semibold rounded-full <%= @game.physical? ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' %>">
<%= @game.format.titleize %>
</span>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date Added</label>
<p class="text-gray-900"><%= @game.date_added.strftime("%B %d, %Y") %></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Completion Status</label>
<p class="text-gray-900"><%= @game.completion_status&.titleize || "Not set" %></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Rating</label>
<p class="text-gray-900"><%= @game.user_rating ? "⭐ #{@game.user_rating}/5" : "Not rated" %></p>
</div>
</div>
<% if @game.genres.any? %>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Genres</label>
<div class="flex flex-wrap gap-2">
<% @game.genres.each do |genre| %>
<span class="px-3 py-1 bg-gray-200 text-gray-700 rounded-full text-sm"><%= genre.name %></span>
<% end %>
</div>
</div>
<% end %>
<% if @game.physical? %>
<div class="mb-6">
<h2 class="text-xl font-bold mb-3">Physical Details</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Condition</label>
<p class="text-gray-900"><%= @game.condition&.titleize || "Not specified" %></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Price Paid</label>
<p class="text-gray-900"><%= @game.price_paid ? "$#{sprintf("%.2f", @game.price_paid)}" : "Not specified" %></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Location</label>
<p class="text-gray-900"><%= @game.location || "Not specified" %></p>
</div>
</div>
</div>
<% end %>
<% if @game.digital? %>
<div class="mb-6">
<h2 class="text-xl font-bold mb-3">Digital Details</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Digital Store</label>
<p class="text-gray-900"><%= @game.digital_store || "Not specified" %></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Price Paid</label>
<p class="text-gray-900"><%= @game.price_paid ? "$#{sprintf("%.2f", @game.price_paid)}" : "Not specified" %></p>
</div>
</div>
</div>
<% end %>
<% if @game.notes.present? %>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-1">Notes</label>
<p class="text-gray-900 whitespace-pre-wrap"><%= @game.notes %></p>
</div>
<% end %>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Collections</label>
<% if @game.collections.any? %>
<div class="flex flex-wrap gap-2">
<% @game.collections.each do |collection| %>
<%= link_to collection.name, collection, class: "px-3 py-1 bg-indigo-100 text-indigo-700 rounded-full text-sm hover:bg-indigo-200" %>
<% end %>
</div>
<% else %>
<p class="text-gray-500 text-sm">Not in any collections. <%= link_to "Edit game", edit_game_path(@game), class: "text-indigo-600 hover:text-indigo-800" %> to add to collections.</p>
<% end %>
</div>
<div class="mt-6 pt-6 border-t">
<%= link_to "← Back to Games", games_path, class: "text-indigo-600 hover:text-indigo-800" %>
</div>
</div>
</div>

View File

@@ -0,0 +1,4 @@
<div>
<h1 class="font-bold text-4xl">Games#update</h1>
<p>Find me in app/views/games/update.html.erb</p>
</div>