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,4 @@
<div>
<h1 class="font-bold text-4xl">ApiTokens#create</h1>
<p>Find me in app/views/api_tokens/create.html.erb</p>
</div>

View File

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

View File

@@ -0,0 +1,118 @@
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-6">API Tokens</h1>
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-yellow-700">
<strong>Important:</strong> Your API token will only be shown once when created. Make sure to copy it and store it securely!
</p>
</div>
</div>
</div>
<div class="bg-white p-6 rounded-lg shadow mb-6">
<h2 class="text-xl font-bold mb-4">Create New Token</h2>
<%= form_with model: @api_token, url: api_tokens_path, class: "space-y-4" do |f| %>
<% if @api_token.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<ul>
<% @api_token.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :name, "Token Name (e.g., 'Mobile App', 'Third Party Integration')", class: "block text-sm font-medium text-gray-700" %>
<%= f.text_field :name, 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 :expires_at, "Expiration Date (optional)", class: "block text-sm font-medium text-gray-700" %>
<%= f.datetime_local_field :expires_at, 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">Leave blank for tokens that never expire</p>
</div>
<div>
<%= f.submit "Create Token", class: "px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700" %>
</div>
<% end %>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Your API Tokens</h2>
<% if @api_tokens.any? %>
<div class="space-y-4">
<% @api_tokens.each do |token| %>
<div class="border border-gray-200 p-4 rounded-lg">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="font-semibold text-lg"><%= token.name || "Unnamed Token" %></div>
<div class="mt-2 space-y-1 text-sm text-gray-600">
<div>
<strong>Token:</strong>
<code class="bg-gray-100 px-2 py-1 rounded font-mono text-xs">
<%= token.token[0..15] %>...
</code>
</div>
<div><strong>Created:</strong> <%= token.created_at.strftime("%B %d, %Y at %I:%M %p") %></div>
<% if token.last_used_at %>
<div><strong>Last Used:</strong> <%= time_ago_in_words(token.last_used_at) %> ago</div>
<% else %>
<div><strong>Last Used:</strong> Never</div>
<% end %>
<% if token.expires_at %>
<div class="<%= token.expired? ? 'text-red-600' : '' %>">
<strong>Expires:</strong> <%= token.expires_at.strftime("%B %d, %Y") %>
<%= " (EXPIRED)" if token.expired? %>
</div>
<% else %>
<div><strong>Expires:</strong> Never</div>
<% end %>
</div>
</div>
<div>
<%= button_to "Delete", api_token_path(token), method: :delete, data: { turbo_confirm: "Are you sure you want to delete this API token? Apps using this token will stop working. This action cannot be undone." }, class: "px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 text-sm" %>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<p class="text-gray-500">You haven't created any API tokens yet. Create one above to start using the API.</p>
<% end %>
</div>
<div class="mt-6 bg-blue-50 border-l-4 border-blue-400 p-4">
<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>API Documentation:</strong> See <code class="bg-blue-100 px-2 py-1 rounded">API_DOCUMENTATION.md</code> for complete API reference and usage examples.
</p>
</div>
</div>
</div>
<div class="mt-4">
<%= link_to "← Back to Settings", settings_path, class: "text-indigo-600 hover:text-indigo-800" %>
</div>
</div>

View File

@@ -0,0 +1,32 @@
<%= form_with model: collection, class: "space-y-6" do |f| %>
<% if collection.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<ul>
<% collection.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :name, class: "block text-sm font-medium text-gray-700" %>
<%= f.text_field :name, 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 :description, class: "block text-sm font-medium text-gray-700" %>
<%= f.text_area :description, rows: 4, 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 :parent_collection_id, "Parent Collection (optional)", class: "block text-sm font-medium text-gray-700" %>
<%= f.collection_select :parent_collection_id, @collections, :id, :name, { include_blank: "None (Root Collection)" }, 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">Subcollections can only be one level deep</p>
</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", collection.persisted? ? collection : collections_path, class: "px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300" %>
</div>
<% end %>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
<div>
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">My Collections</h1>
<%= link_to "New Collection", new_collection_path, class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
</div>
<% if @root_collections.any? %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<% @root_collections.each do |collection| %>
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-2">
<%= link_to collection.name, collection, class: "text-indigo-600 hover:text-indigo-800" %>
</h2>
<% if collection.description.present? %>
<p class="text-gray-600 mb-4"><%= truncate(collection.description, length: 100) %></p>
<% end %>
<div class="text-sm text-gray-500 mb-4">
<%= pluralize(collection.game_count, "game") %>
</div>
<% if collection.subcollections.any? %>
<div class="mb-4">
<p class="text-sm font-medium text-gray-700 mb-2">Subcollections:</p>
<div class="space-y-1">
<% collection.subcollections.each do |subcollection| %>
<div class="text-sm">
<%= link_to subcollection.name, subcollection, class: "text-indigo-600 hover:text-indigo-800" %>
(<%= pluralize(subcollection.game_count, "game") %>)
</div>
<% end %>
</div>
</div>
<% end %>
<div class="flex space-x-2 mt-4">
<%= link_to "View", collection, class: "text-indigo-600 hover:text-indigo-800 text-sm" %>
<%= link_to "Edit", edit_collection_path(collection), class: "text-blue-600 hover:text-blue-800 text-sm" %>
<%= button_to "Delete", collection, method: :delete, data: { turbo_confirm: "Are you sure you want to delete the collection '#{collection.name}'? Games in this collection will not be deleted." }, class: "text-red-600 hover:text-red-800 text-sm" %>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="bg-white p-8 rounded-lg shadow text-center">
<p class="text-gray-500 mb-4">You haven't created any collections yet.</p>
<%= link_to "Create Your First Collection", new_collection_path, class: "text-indigo-600 hover:text-indigo-800" %>
</div>
<% end %>
</div>

View File

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

View File

@@ -0,0 +1,104 @@
<div>
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-3xl font-bold"><%= @collection.name %></h1>
<% if @collection.subcollection? %>
<p class="text-gray-600 mt-1">
Subcollection of <%= link_to @collection.parent_collection.name, @collection.parent_collection, class: "text-indigo-600 hover:text-indigo-800" %>
</p>
<% end %>
</div>
<div class="flex space-x-2">
<%= link_to "Edit", edit_collection_path(@collection), class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
<%= button_to "Delete", @collection, method: :delete, data: { turbo_confirm: "Are you sure you want to delete '#{@collection.name}'? This will permanently remove the collection but games in it will not be deleted." }, class: "px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700" %>
</div>
</div>
<% if @collection.description.present? %>
<div class="bg-white p-6 rounded-lg shadow mb-6">
<p class="text-gray-700"><%= @collection.description %></p>
</div>
<% end %>
<div class="bg-white p-6 rounded-lg shadow mb-6">
<div class="grid grid-cols-2 gap-4">
<div>
<div class="text-gray-500 text-sm">Total Games</div>
<div class="text-3xl font-bold"><%= @collection.game_count %></div>
</div>
<% if @collection.subcollections.any? %>
<div>
<div class="text-gray-500 text-sm">Subcollections</div>
<div class="text-3xl font-bold"><%= @collection.subcollections.count %></div>
</div>
<% end %>
</div>
</div>
<% if @collection.subcollections.any? %>
<div class="bg-white p-6 rounded-lg shadow mb-6">
<h2 class="text-xl font-bold mb-4">Subcollections</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<% @collection.subcollections.each do |subcollection| %>
<div class="border border-gray-200 p-4 rounded-lg">
<%= link_to subcollection.name, subcollection, class: "font-semibold text-indigo-600 hover:text-indigo-800" %>
<div class="text-sm text-gray-500 mt-1">
<%= pluralize(subcollection.game_count, "game") %>
</div>
</div>
<% end %>
</div>
</div>
<% end %>
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Games in Collection</h2>
<% if @games.any? %>
<div class="space-y-4">
<% @games.each do |game| %>
<div class="border-b pb-4 last:border-b-0">
<div class="flex justify-between items-start">
<div>
<%= link_to game.title, game, class: "text-lg font-semibold text-indigo-600 hover:text-indigo-800" %>
<div class="text-sm text-gray-500 mt-1">
<%= game.platform.name %>
·
<span class="<%= game.physical? ? 'text-blue-600' : 'text-green-600' %>">
<%= game.format.titleize %>
</span>
<% if game.completion_status %>
· <%= game.completion_status.titleize %>
<% end %>
<% if game.user_rating %>
· ⭐ <%= game.user_rating %>/5
<% end %>
</div>
<% if game.genres.any? %>
<div class="mt-2 flex flex-wrap gap-1">
<% game.genres.each do |genre| %>
<span class="px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs"><%= genre.name %></span>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<% if @games.respond_to?(:total_pages) && @games.total_pages > 1 %>
<div class="mt-6">
<%#= paginate @games %>
</div>
<% end %>
<% else %>
<p class="text-gray-500">No games in this collection yet. <%= link_to "Add games", games_path, class: "text-indigo-600 hover:text-indigo-800" %> to get started.</p>
<% end %>
</div>
<div class="mt-6">
<%= link_to "← Back to Collections", collections_path, class: "text-indigo-600 hover:text-indigo-800" %>
</div>
</div>

View File

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

View File

@@ -0,0 +1,116 @@
<div>
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold">Dashboard</h1>
<%= link_to "Add Game", new_game_path, class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Total Games</div>
<div class="text-3xl font-bold"><%= @total_games %></div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Physical Games</div>
<div class="text-3xl font-bold"><%= @physical_games %></div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Digital Games</div>
<div class="text-3xl font-bold"><%= @digital_games %></div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Total Spent</div>
<div class="text-3xl font-bold">$<%= sprintf("%.2f", @total_spent || 0) %></div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Completed</div>
<div class="text-3xl font-bold"><%= @completed_games %></div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Backlog</div>
<div class="text-3xl font-bold"><%= @backlog_games %></div>
</div>
</div>
<!-- Charts -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Top Platforms</h2>
<% if @games_by_platform.any? %>
<ul class="space-y-2">
<% @games_by_platform.each do |platform, count| %>
<li class="flex justify-between">
<span><%= platform %></span>
<span class="font-bold"><%= count %></span>
</li>
<% end %>
</ul>
<% else %>
<p class="text-gray-500">No games yet</p>
<% end %>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Top Genres</h2>
<% if @games_by_genre.any? %>
<ul class="space-y-2">
<% @games_by_genre.each do |genre, count| %>
<li class="flex justify-between">
<span><%= genre %></span>
<span class="font-bold"><%= count %></span>
</li>
<% end %>
</ul>
<% else %>
<p class="text-gray-500">No games yet</p>
<% end %>
</div>
</div>
<!-- Recently Added Games -->
<div class="bg-white p-6 rounded-lg shadow mb-8">
<h2 class="text-xl font-bold mb-4">Recently Added</h2>
<% if @recently_added_games.any? %>
<div class="space-y-4">
<% @recently_added_games.each do |game| %>
<div class="flex justify-between items-center border-b pb-2">
<div>
<%= link_to game.title, game, class: "font-semibold text-indigo-600 hover:text-indigo-800" %>
<div class="text-sm text-gray-500"><%= game.platform.name %></div>
</div>
<div class="text-sm text-gray-500"><%= game.date_added.strftime("%b %d, %Y") %></div>
</div>
<% end %>
</div>
<%= link_to "View All Games", games_path, class: "mt-4 inline-block text-indigo-600 hover:text-indigo-800" %>
<% else %>
<p class="text-gray-500">No games in your collection yet. <%= link_to "Add your first game", new_game_path, class: "text-indigo-600 hover:text-indigo-800" %></p>
<% end %>
</div>
<!-- Currently Playing -->
<div class="bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Currently Playing</h2>
<% if @currently_playing_games.any? %>
<div class="space-y-4">
<% @currently_playing_games.each do |game| %>
<div class="flex justify-between items-center border-b pb-2">
<div>
<%= link_to game.title, game, class: "font-semibold text-indigo-600 hover:text-indigo-800" %>
<div class="text-sm text-gray-500"><%= game.platform.name %></div>
</div>
</div>
<% end %>
</div>
<% else %>
<p class="text-gray-500">Not currently playing anything</p>
<% end %>
</div>
</div>

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>

View File

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

View File

@@ -0,0 +1,193 @@
<%
# Auto-refresh if user just started a sync or has games being processed
has_unmatched = @unmatched_games > 0 && @pending_review_count == 0
just_synced = current_user.igdb_last_synced_at && current_user.igdb_last_synced_at > 5.minutes.ago
should_auto_refresh = has_unmatched && just_synced
%>
<% if should_auto_refresh %>
<meta http-equiv="refresh" content="30">
<div class="bg-blue-100 border-l-4 border-blue-500 p-4 mb-4">
<div class="flex items-center">
<svg class="animate-spin h-5 w-5 text-blue-500 mr-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="text-blue-700">Processing games... Page will auto-refresh every 30 seconds.</span>
</div>
</div>
<% end %>
<div class="max-w-6xl mx-auto">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">IGDB Game Matching</h1>
<%= button_to "Sync Now", sync_now_igdb_matches_path, method: :post, class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
</div>
<!-- Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Matched Games</div>
<div class="text-3xl font-bold text-green-600"><%= @matched_games %></div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Unmatched Games</div>
<div class="text-3xl font-bold text-gray-600"><%= @unmatched_games %></div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Pending Review</div>
<div class="text-3xl font-bold text-yellow-600"><%= @pending_review_count %></div>
</div>
</div>
<% if current_user.igdb_last_synced_at %>
<div class="bg-blue-50 border-l-4 border-blue-400 p-4 mb-6">
<p class="text-sm text-blue-700">
<strong>Last synced:</strong> <%= time_ago_in_words(current_user.igdb_last_synced_at) %> ago
</p>
</div>
<% end %>
<% if @pending_suggestions.empty? %>
<div class="bg-white p-8 rounded-lg shadow text-center">
<% if @unmatched_games > 0 %>
<p class="text-gray-600 mb-4">No pending matches to review. Click "Sync Now" to search IGDB for your games!</p>
<%= button_to "Sync Now", sync_now_igdb_matches_path, method: :post, class: "px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700" %>
<% else %>
<p class="text-gray-600">All your games are matched with IGDB! 🎉</p>
<% end %>
</div>
<% else %>
<div class="space-y-8">
<% @pending_suggestions.each do |game, suggestions| %>
<div class="bg-white rounded-lg shadow overflow-hidden">
<!-- Game Header -->
<div class="bg-gray-50 px-6 py-4 border-b">
<div class="flex justify-between items-start">
<div>
<h2 class="text-xl font-bold text-gray-900"><%= game.title %></h2>
<p class="text-sm text-gray-600">
<%= game.platform.name %> · <%= game.format.titleize %>
<% if game.date_added %>
· Added <%= game.date_added.strftime("%b %Y") %>
<% end %>
</p>
</div>
<span class="px-3 py-1 text-sm font-semibold rounded-full <%=
case game.igdb_match_status
when 'high_confidence' then 'bg-green-100 text-green-800'
when 'medium_confidence' then 'bg-yellow-100 text-yellow-800'
else 'bg-gray-100 text-gray-800'
end
%>">
<%= game.igdb_match_status&.titleize || 'Needs Review' %>
</span>
</div>
</div>
<!-- Suggestions -->
<div class="p-6">
<h3 class="text-lg font-semibold mb-4">
Suggested Matches from IGDB:
</h3>
<div class="space-y-4">
<% suggestions.each do |suggestion| %>
<div class="border rounded-lg p-4 hover:bg-gray-50 transition">
<div class="flex gap-4">
<!-- Cover Image -->
<div class="flex-shrink-0">
<% if suggestion.igdb_cover_url.present? %>
<%= image_tag suggestion.cover_image_url("cover_big"),
alt: suggestion.igdb_name,
class: "w-24 h-32 object-cover rounded shadow" %>
<% else %>
<div class="w-24 h-32 bg-gray-200 rounded flex items-center justify-center">
<span class="text-gray-400 text-xs">No Cover</span>
</div>
<% end %>
</div>
<!-- Match Info -->
<div class="flex-1">
<div class="flex justify-between items-start mb-2">
<div class="flex-1">
<h4 class="text-lg font-semibold text-gray-900"><%= suggestion.igdb_name %></h4>
<div class="text-sm text-gray-600 space-x-2 mb-2">
<span><%= suggestion.igdb_platform_name %></span>
<% if suggestion.igdb_release_date %>
<span>·</span>
<span><%= suggestion.igdb_release_date.year %></span>
<% end %>
</div>
<!-- Summary -->
<% if suggestion.igdb_summary.present? %>
<p class="text-sm text-gray-700 leading-relaxed mb-2" style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">
<%= suggestion.igdb_summary %>
</p>
<% end %>
<!-- Genres -->
<% if suggestion.igdb_genres.present? && suggestion.igdb_genres.any? %>
<div class="flex flex-wrap gap-1 mt-2">
<% suggestion.igdb_genres.first(5).each do |genre| %>
<span class="px-2 py-0.5 bg-indigo-100 text-indigo-700 rounded text-xs">
<%= genre %>
</span>
<% end %>
</div>
<% end %>
</div>
<!-- Confidence Score -->
<div class="text-right">
<div class="text-2xl font-bold <%=
if suggestion.confidence_score >= 80
'text-green-600'
elsif suggestion.confidence_score >= 60
'text-yellow-600'
else
'text-gray-600'
end
%>">
<%= suggestion.confidence_score.to_i %>%
</div>
<div class="text-xs text-gray-500">match</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-2 mt-4">
<%= button_to "✓ Approve This Match", approve_igdb_match_path(suggestion),
method: :post,
class: "px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 text-sm font-medium",
data: { turbo_confirm: "Link '#{suggestion.game.title}' to '#{suggestion.igdb_name}'? This will import cover art, genres, and metadata." } %>
<%= button_to "✗ Not This One", reject_igdb_match_path(suggestion),
method: :post,
class: "px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 text-sm font-medium",
data: { turbo_confirm: "Reject this match for '#{suggestion.game.title}'?" } %>
</div>
</div>
</div>
</div>
<% end %>
</div>
<!-- Reject All Option -->
<div class="mt-6 pt-6 border-t">
<p class="text-sm text-gray-600 mb-2">None of these match?</p>
<%= button_to "Reject All Suggestions", reject_igdb_match_path(suggestions.first),
method: :post,
class: "px-4 py-2 bg-red-100 text-red-700 rounded hover:bg-red-200 text-sm font-medium",
data: { turbo_confirm: "Reject all #{suggestions.count} suggestions for '#{game.title}'? You can manually match it later if needed." } %>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
<div class="container mx-auto px-4 mt-4">
<% if flash[:notice] %>
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4" role="alert">
<span class="block sm:inline"><%= flash[:notice] %></span>
</div>
<% end %>
<% if flash[:alert] %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert">
<span class="block sm:inline"><%= flash[:alert] %></span>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,77 @@
<footer class="bg-gray-800 text-white mt-12">
<div class="max-w-7xl mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- About -->
<div>
<h3 class="text-lg font-bold mb-4">TurboVault</h3>
<p class="text-gray-400 text-sm">
Your personal video game collection tracker. Organize, track, and showcase your gaming library.
</p>
</div>
<!-- Quick Links -->
<div>
<h3 class="text-lg font-bold mb-4">Quick Links</h3>
<ul class="space-y-2 text-sm">
<% if user_signed_in? %>
<li><%= link_to "Dashboard", dashboard_path, class: "text-gray-400 hover:text-white" %></li>
<li><%= link_to "Games", games_path, class: "text-gray-400 hover:text-white" %></li>
<li><%= link_to "Collections", collections_path, class: "text-gray-400 hover:text-white" %></li>
<li><%= link_to "Settings", settings_path, class: "text-gray-400 hover:text-white" %></li>
<% else %>
<li><%= link_to "Home", root_path, class: "text-gray-400 hover:text-white" %></li>
<li><%= link_to "Sign Up", signup_path, class: "text-gray-400 hover:text-white" %></li>
<li><%= link_to "Login", login_path, class: "text-gray-400 hover:text-white" %></li>
<% end %>
</ul>
</div>
<!-- Features -->
<div>
<h3 class="text-lg font-bold mb-4">Features</h3>
<ul class="space-y-2 text-sm text-gray-400">
<li>📚 Track Physical & Digital Games</li>
<li>🎮 IGDB Integration</li>
<li>📊 Collection Statistics</li>
<li>🔐 RESTful API Access</li>
</ul>
</div>
<!-- Resources -->
<div>
<h3 class="text-lg font-bold mb-4">Resources</h3>
<ul class="space-y-2 text-sm">
<li><%= link_to "API Documentation", api_docs_path, class: "text-gray-400 hover:text-white" %></li>
<li>
<a href="https://github.com/yourusername/turbovault" target="_blank" class="text-gray-400 hover:text-white">
GitHub
</a>
</li>
<li>
<a href="https://www.igdb.com" target="_blank" class="text-gray-400 hover:text-white">
Powered by IGDB
</a>
</li>
</ul>
</div>
</div>
<!-- Bottom Bar -->
<div class="border-t border-gray-700 mt-8 pt-8 text-center text-sm text-gray-400">
<p>&copy; <%= Time.current.year %> TurboVault. Built with Rails 8 & Hotwire.</p>
<p class="mt-1">
Game data supplied by
<a href="https://www.igdb.com/" target="_blank" rel="noopener noreferrer" class="text-indigo-400 hover:text-indigo-300">
IGDB
</a>
</p>
<p class="mt-2">
<% if user_signed_in? %>
Logged in as <strong><%= current_user.username %></strong>
<% else %>
<%= link_to "Create an account", signup_path, class: "text-indigo-400 hover:text-indigo-300" %> to get started
<% end %>
</p>
</div>
</div>
</footer>

View File

@@ -0,0 +1,47 @@
<nav class="bg-white shadow-lg">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center py-4">
<div class="flex items-center space-x-8">
<%= link_to "TurboVault", root_path, class: "text-2xl font-bold text-indigo-600" %>
<% if user_signed_in? %>
<div class="hidden md:flex space-x-4">
<%= link_to "Dashboard", dashboard_path, class: "text-gray-700 hover:text-indigo-600" %>
<%= link_to "Games", games_path, class: "text-gray-700 hover:text-indigo-600" %>
<%= link_to "Collections", collections_path, class: "text-gray-700 hover:text-indigo-600" %>
<% if current_user.igdb_sync_enabled? %>
<%
# Count games with pending suggestions (not total suggestions)
pending_count = current_user.games
.igdb_unmatched
.joins(:igdb_match_suggestions)
.where(igdb_match_suggestions: { status: 'pending' })
.distinct
.count
%>
<%= link_to igdb_matches_path, class: "text-gray-700 hover:text-indigo-600 relative" do %>
IGDB
<% if pending_count > 0 %>
<span class="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
<%= pending_count %>
</span>
<% end %>
<% end %>
<% end %>
</div>
<% end %>
</div>
<div class="flex items-center space-x-4">
<% if user_signed_in? %>
<%= link_to "Profile", profile_path(current_user.username), class: "text-gray-700 hover:text-indigo-600" %>
<%= link_to "Settings", settings_path, class: "text-gray-700 hover:text-indigo-600" %>
<%= button_to "Logout", logout_path, method: :delete, class: "px-4 py-2 bg-gray-200 rounded hover:bg-gray-300" %>
<% else %>
<%= link_to "Login", login_path, class: "text-gray-700 hover:text-indigo-600" %>
<%= link_to "Sign Up", signup_path, class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
<% end %>
</div>
</div>
</div>
</nav>

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<title><%= content_for(:title) || "Turbovault Web" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="application-name" content="Turbovault Web">
<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) %>
<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 "application", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "themes", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body class="<%= user_signed_in? ? current_user.theme_class : 'theme-light' %> bg-gray-50 flex flex-col min-h-screen">
<%= render "layouts/navigation" %>
<%= render "layouts/flash" if flash.any? %>
<main class="container mx-auto px-4 py-8 flex-grow">
<%= yield %>
</main>
<%= render "layouts/footer" %>
</body>
</html>

View File

@@ -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>

View File

@@ -0,0 +1 @@
<%= yield %>

View File

@@ -0,0 +1,377 @@
<div class="max-w-5xl mx-auto">
<div class="bg-white rounded-lg shadow-lg p-8 mb-6">
<h1 class="text-4xl font-bold text-gray-900 mb-4">TurboVault API Documentation</h1>
<p class="text-lg text-gray-600 mb-6">
RESTful API for accessing and managing your video game collection programmatically.
</p>
<!-- Quick Start -->
<div class="bg-indigo-50 border-l-4 border-indigo-500 p-4 mb-6">
<h2 class="text-xl font-bold text-indigo-900 mb-2">Quick Start</h2>
<ol class="list-decimal list-inside space-y-2 text-indigo-800">
<li><%= link_to "Generate an API token", settings_api_tokens_path, class: "underline hover:text-indigo-600" %> in your settings</li>
<li>Include the token in the <code class="bg-indigo-100 px-2 py-1 rounded">Authorization</code> header</li>
<li>Make requests to <code class="bg-indigo-100 px-2 py-1 rounded">https://yourdomain.com/api/v1/...</code></li>
</ol>
</div>
</div>
<!-- Table of Contents -->
<div class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-2xl font-bold mb-4">Table of Contents</h2>
<ul class="space-y-2">
<li><a href="#authentication" class="text-indigo-600 hover:text-indigo-800">Authentication</a></li>
<li><a href="#games" class="text-indigo-600 hover:text-indigo-800">Games</a></li>
<li><a href="#collections" class="text-indigo-600 hover:text-indigo-800">Collections</a></li>
<li><a href="#platforms" class="text-indigo-600 hover:text-indigo-800">Platforms</a></li>
<li><a href="#genres" class="text-indigo-600 hover:text-indigo-800">Genres</a></li>
<li><a href="#errors" class="text-indigo-600 hover:text-indigo-800">Error Handling</a></li>
<li><a href="#rate-limits" class="text-indigo-600 hover:text-indigo-800">Rate Limits</a></li>
</ul>
</div>
<!-- Authentication -->
<div id="authentication" class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-2xl font-bold mb-4">Authentication</h2>
<p class="text-gray-700 mb-4">All API requests require authentication using a bearer token.</p>
<div class="bg-gray-900 text-gray-100 p-4 rounded mb-4 overflow-x-auto">
<pre><code>Authorization: Bearer YOUR_API_TOKEN_HERE</code></pre>
</div>
<p class="text-sm text-gray-600 mb-4">
<%= link_to "Generate a token", settings_api_tokens_path, class: "text-indigo-600 hover:text-indigo-800" %> in your settings page.
</p>
<div class="bg-yellow-50 border-l-4 border-yellow-500 p-4">
<p class="text-sm text-yellow-800">
<strong>Security:</strong> Keep your API tokens secure. Do not share them or commit them to version control.
</p>
</div>
</div>
<!-- Games Endpoints -->
<div id="games" class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-2xl font-bold mb-4">Games</h2>
<!-- List Games -->
<div class="mb-8 pb-8 border-b">
<h3 class="text-xl font-semibold mb-2">List Games</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-green-100 text-green-800 font-mono text-sm rounded">GET</span>
<code class="text-gray-700">/api/v1/games</code>
</div>
<p class="text-gray-700 mb-4">Returns a paginated list of your games.</p>
<h4 class="font-semibold mb-2">Query Parameters:</h4>
<table class="w-full text-sm mb-4">
<tr class="border-b">
<td class="py-2 font-mono">page</td>
<td class="py-2 text-gray-600">Page number (default: 1)</td>
</tr>
<tr class="border-b">
<td class="py-2 font-mono">per_page</td>
<td class="py-2 text-gray-600">Items per page (default: 50, max: 100)</td>
</tr>
<tr class="border-b">
<td class="py-2 font-mono">format</td>
<td class="py-2 text-gray-600">Filter by format: <code>physical</code> or <code>digital</code></td>
</tr>
<tr class="border-b">
<td class="py-2 font-mono">platform_id</td>
<td class="py-2 text-gray-600">Filter by platform ID</td>
</tr>
<tr class="border-b">
<td class="py-2 font-mono">genre_id</td>
<td class="py-2 text-gray-600">Filter by genre ID</td>
</tr>
</table>
<h4 class="font-semibold mb-2">Example Request:</h4>
<div class="bg-gray-900 text-gray-100 p-4 rounded mb-4 overflow-x-auto">
<pre><code>curl -X GET "https://yourdomain.com/api/v1/games?page=1&per_page=10" \
-H "Authorization: Bearer YOUR_TOKEN"</code></pre>
</div>
<h4 class="font-semibold mb-2">Example Response:</h4>
<div class="bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto">
<pre><code>{
"games": [
{
"id": 1,
"title": "The Legend of Zelda: Ocarina of Time",
"platform": "Nintendo 64",
"format": "physical",
"date_added": "2024-01-15",
"completion_status": "completed",
"user_rating": 5,
"igdb_id": 1234,
"condition": "very_good",
"price_paid": "45.99",
"location": "Bedroom Shelf A",
"genres": ["Action", "Adventure"],
"collections": ["Favorites", "N64 Collection"]
}
],
"pagination": {
"current_page": 1,
"per_page": 10,
"total_pages": 3,
"total_count": 25
}
}</code></pre>
</div>
</div>
<!-- Get Single Game -->
<div class="mb-8 pb-8 border-b">
<h3 class="text-xl font-semibold mb-2">Get a Game</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-green-100 text-green-800 font-mono text-sm rounded">GET</span>
<code class="text-gray-700">/api/v1/games/:id</code>
</div>
<p class="text-gray-700 mb-4">Returns details for a specific game.</p>
<h4 class="font-semibold mb-2">Example Request:</h4>
<div class="bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto">
<pre><code>curl -X GET "https://yourdomain.com/api/v1/games/1" \
-H "Authorization: Bearer YOUR_TOKEN"</code></pre>
</div>
</div>
<!-- Create Game -->
<div class="mb-8 pb-8 border-b">
<h3 class="text-xl font-semibold mb-2">Create a Game</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-blue-100 text-blue-800 font-mono text-sm rounded">POST</span>
<code class="text-gray-700">/api/v1/games</code>
</div>
<p class="text-gray-700 mb-4">Add a new game to your collection.</p>
<h4 class="font-semibold mb-2">Request Body:</h4>
<div class="bg-gray-900 text-gray-100 p-4 rounded mb-4 overflow-x-auto">
<pre><code>{
"game": {
"title": "Super Mario 64",
"platform_id": 4,
"format": "physical",
"date_added": "2024-03-28",
"completion_status": "backlog",
"user_rating": null,
"condition": "good",
"price_paid": "35.00",
"location": "Living Room Shelf",
"genre_ids": [1, 3],
"collection_ids": [2]
}
}</code></pre>
</div>
</div>
<!-- Update Game -->
<div class="mb-8 pb-8 border-b">
<h3 class="text-xl font-semibold mb-2">Update a Game</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-yellow-100 text-yellow-800 font-mono text-sm rounded">PUT/PATCH</span>
<code class="text-gray-700">/api/v1/games/:id</code>
</div>
<p class="text-gray-700 mb-4">Update an existing game's information.</p>
</div>
<!-- Delete Game -->
<div class="mb-8">
<h3 class="text-xl font-semibold mb-2">Delete a Game</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-red-100 text-red-800 font-mono text-sm rounded">DELETE</span>
<code class="text-gray-700">/api/v1/games/:id</code>
</div>
<p class="text-gray-700 mb-4">Permanently delete a game from your collection.</p>
</div>
</div>
<!-- Collections Endpoints -->
<div id="collections" class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-2xl font-bold mb-4">Collections</h2>
<div class="mb-6">
<h3 class="text-xl font-semibold mb-2">List Collections</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-green-100 text-green-800 font-mono text-sm rounded">GET</span>
<code class="text-gray-700">/api/v1/collections</code>
</div>
</div>
<div class="mb-6">
<h3 class="text-xl font-semibold mb-2">Get a Collection</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-green-100 text-green-800 font-mono text-sm rounded">GET</span>
<code class="text-gray-700">/api/v1/collections/:id</code>
</div>
<p class="text-gray-700 text-sm">Includes all games in the collection.</p>
</div>
<div class="mb-6">
<h3 class="text-xl font-semibold mb-2">Create a Collection</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-blue-100 text-blue-800 font-mono text-sm rounded">POST</span>
<code class="text-gray-700">/api/v1/collections</code>
</div>
</div>
<div class="mb-6">
<h3 class="text-xl font-semibold mb-2">Update a Collection</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-yellow-100 text-yellow-800 font-mono text-sm rounded">PUT/PATCH</span>
<code class="text-gray-700">/api/v1/collections/:id</code>
</div>
</div>
<div>
<h3 class="text-xl font-semibold mb-2">Delete a Collection</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-red-100 text-red-800 font-mono text-sm rounded">DELETE</span>
<code class="text-gray-700">/api/v1/collections/:id</code>
</div>
<p class="text-gray-700 text-sm">Games in the collection will not be deleted.</p>
</div>
</div>
<!-- Platforms -->
<div id="platforms" class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-2xl font-bold mb-4">Platforms</h2>
<div class="mb-6">
<h3 class="text-xl font-semibold mb-2">List Platforms</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-green-100 text-green-800 font-mono text-sm rounded">GET</span>
<code class="text-gray-700">/api/v1/platforms</code>
</div>
<p class="text-gray-700 text-sm">Returns all available gaming platforms.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-2">Get a Platform</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-green-100 text-green-800 font-mono text-sm rounded">GET</span>
<code class="text-gray-700">/api/v1/platforms/:id</code>
</div>
</div>
</div>
<!-- Genres -->
<div id="genres" class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-2xl font-bold mb-4">Genres</h2>
<div class="mb-6">
<h3 class="text-xl font-semibold mb-2">List Genres</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-green-100 text-green-800 font-mono text-sm rounded">GET</span>
<code class="text-gray-700">/api/v1/genres</code>
</div>
<p class="text-gray-700 text-sm">Returns all available game genres.</p>
</div>
<div>
<h3 class="text-xl font-semibold mb-2">Get a Genre</h3>
<div class="flex items-center gap-2 mb-4">
<span class="px-3 py-1 bg-green-100 text-green-800 font-mono text-sm rounded">GET</span>
<code class="text-gray-700">/api/v1/genres/:id</code>
</div>
</div>
</div>
<!-- Error Handling -->
<div id="errors" class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-2xl font-bold mb-4">Error Handling</h2>
<p class="text-gray-700 mb-4">The API uses standard HTTP response codes:</p>
<table class="w-full text-sm mb-4">
<thead class="bg-gray-50">
<tr>
<th class="py-2 px-4 text-left">Code</th>
<th class="py-2 px-4 text-left">Meaning</th>
</tr>
</thead>
<tbody>
<tr class="border-b">
<td class="py-2 px-4 font-mono">200</td>
<td class="py-2 px-4 text-gray-600">Success</td>
</tr>
<tr class="border-b">
<td class="py-2 px-4 font-mono">201</td>
<td class="py-2 px-4 text-gray-600">Created</td>
</tr>
<tr class="border-b">
<td class="py-2 px-4 font-mono">400</td>
<td class="py-2 px-4 text-gray-600">Bad Request - Invalid parameters</td>
</tr>
<tr class="border-b">
<td class="py-2 px-4 font-mono">401</td>
<td class="py-2 px-4 text-gray-600">Unauthorized - Invalid or missing token</td>
</tr>
<tr class="border-b">
<td class="py-2 px-4 font-mono">404</td>
<td class="py-2 px-4 text-gray-600">Not Found</td>
</tr>
<tr class="border-b">
<td class="py-2 px-4 font-mono">422</td>
<td class="py-2 px-4 text-gray-600">Unprocessable Entity - Validation errors</td>
</tr>
<tr class="border-b">
<td class="py-2 px-4 font-mono">429</td>
<td class="py-2 px-4 text-gray-600">Too Many Requests - Rate limit exceeded</td>
</tr>
<tr>
<td class="py-2 px-4 font-mono">500</td>
<td class="py-2 px-4 text-gray-600">Internal Server Error</td>
</tr>
</tbody>
</table>
<h4 class="font-semibold mb-2">Error Response Format:</h4>
<div class="bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto">
<pre><code>{
"error": "Validation failed",
"details": {
"title": ["can't be blank"],
"platform_id": ["must exist"]
}
}</code></pre>
</div>
</div>
<!-- Rate Limits -->
<div id="rate-limits" class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-2xl font-bold mb-4">Rate Limits</h2>
<p class="text-gray-700 mb-4">
API requests are currently not rate-limited, but this may change in the future.
Please be respectful and avoid excessive requests.
</p>
<div class="bg-blue-50 border-l-4 border-blue-500 p-4">
<p class="text-sm text-blue-800">
<strong>Best Practice:</strong> Cache responses when possible and implement exponential backoff for failed requests.
</p>
</div>
</div>
<!-- Support -->
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-2xl font-bold mb-4">Need Help?</h2>
<p class="text-gray-700 mb-4">
If you have questions or encounter issues with the API:
</p>
<ul class="list-disc list-inside space-y-2 text-gray-700">
<li>Check the <%= link_to "API tokens page", settings_api_tokens_path, class: "text-indigo-600 hover:text-indigo-800" %> for troubleshooting</li>
<li>Review this documentation for endpoint details</li>
<li>Contact support if you need assistance</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,26 @@
<div class="max-w-4xl mx-auto text-center py-16">
<h1 class="text-5xl font-bold text-gray-900 mb-6">Welcome to TurboVault</h1>
<p class="text-xl text-gray-600 mb-8">Track and manage your video game collection with ease</p>
<div class="space-x-4">
<%= link_to "Get Started", signup_path, class: "px-8 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-lg font-semibold" %>
<%= link_to "Login", login_path, class: "px-8 py-3 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 text-lg font-semibold" %>
</div>
<div class="mt-16 grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-xl font-bold mb-2">Track Your Collection</h3>
<p class="text-gray-600">Manage both physical and digital games across all platforms</p>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-xl font-bold mb-2">Organize with Collections</h3>
<p class="text-gray-600">Create custom collections and subcollections for your games</p>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-xl font-bold mb-2">View Statistics</h3>
<p class="text-gray-600">See insights about your collection with detailed stats</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: #4F46E5;
color: white;
padding: 20px;
text-align: center;
border-radius: 8px 8px 0 0;
}
.content {
background-color: #f9fafb;
padding: 30px;
border: 1px solid #e5e7eb;
}
.button {
display: inline-block;
background-color: #4F46E5;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 6px;
margin: 20px 0;
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
font-size: 12px;
color: #6b7280;
text-align: center;
}
.warning {
background-color: #fef3c7;
border-left: 4px solid #f59e0b;
padding: 12px;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="header">
<h1>🎮 TurboVault</h1>
</div>
<div class="content">
<h2>Password Reset Request</h2>
<p>Hi <%= @user.username %>,</p>
<p>We received a request to reset your password. Click the button below to create a new password:</p>
<div style="text-align: center;">
<a href="<%= @reset_url %>" class="button">Reset My Password</a>
</div>
<p>Or copy and paste this URL into your browser:</p>
<p style="word-break: break-all; background: white; padding: 10px; border: 1px solid #e5e7eb; border-radius: 4px;">
<%= @reset_url %>
</p>
<div class="warning">
<strong>⚠️ Security Note:</strong> This password reset link will expire in 2 hours for security reasons.
</div>
<p>If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.</p>
</div>
<div class="footer">
<p>This email was sent to <%= @user.email %></p>
<p>&copy; <%= Time.current.year %> TurboVault - Your Game Collection Manager</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,18 @@
TurboVault - Password Reset Request
=====================================
Hi <%= @user.username %>,
We received a request to reset your password.
To reset your password, click the link below:
<%= @reset_url %>
This link will expire in 2 hours for security reasons.
If you didn't request a password reset, you can safely ignore this email.
Your password will remain unchanged.
---
This email was sent to <%= @user.email %>
© <%= Time.current.year %> TurboVault - Your Game Collection Manager

View File

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

View File

@@ -0,0 +1,34 @@
<div class="max-w-md mx-auto">
<h1 class="text-3xl font-bold mb-6">Reset Your Password</h1>
<p class="text-gray-600 mb-6">
Enter your new password below.
</p>
<%= form_with model: @user, url: password_reset_path(params[:id]), method: :patch, class: "space-y-4" do |f| %>
<% if @user.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<ul>
<% @user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :password, "New Password", class: "block text-sm font-medium text-gray-700" %>
<%= f.password_field :password, autofocus: true, required: true, 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">At least 8 characters</p>
</div>
<div>
<%= f.label :password_confirmation, "Confirm New Password", class: "block text-sm font-medium text-gray-700" %>
<%= f.password_field :password_confirmation, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div>
<%= f.submit "Reset Password", class: "w-full px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700" %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,40 @@
<div class="max-w-md mx-auto">
<h1 class="text-3xl font-bold mb-6">Forgot Your Password?</h1>
<p class="text-gray-600 mb-6">
Enter your email address and we'll send you instructions to reset your password.
</p>
<%= form_with url: password_resets_path, method: :post, class: "space-y-4" do |f| %>
<div>
<%= f.label :email, class: "block text-sm font-medium text-gray-700" %>
<%= f.email_field :email, autofocus: true, required: true, placeholder: "your@email.com", class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div>
<%= f.submit "Send Reset Instructions", class: "w-full px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700" %>
</div>
<% end %>
<div class="mt-6 text-center space-y-2">
<%= link_to "← Back to Login", login_path, class: "text-indigo-600 hover:text-indigo-800" %>
</div>
<% if Rails.env.development? %>
<div class="mt-8 p-4 bg-blue-50 border-l-4 border-blue-400 rounded">
<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>Development Mode:</strong> Password reset emails are being captured by Mailpit.
View them at <a href="http://localhost:8025" target="_blank" class="underline font-semibold">localhost:8025</a>
</p>
</div>
</div>
</div>
<% end %>
</div>

View File

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

View File

@@ -0,0 +1,130 @@
<div class="max-w-6xl mx-auto">
<!-- Profile Header -->
<div class="bg-white rounded-lg shadow p-6 mb-6">
<div class="flex justify-between items-start">
<div>
<h1 class="text-3xl font-bold text-gray-900">@<%= @user.username %></h1>
<% if @user.bio.present? %>
<p class="text-gray-600 mt-2"><%= @user.bio %></p>
<% end %>
</div>
<% if current_user && @user == current_user %>
<%= link_to "Edit Profile", settings_path, class: "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700" %>
<% end %>
</div>
</div>
<!-- Statistics -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Total Games</div>
<div class="text-3xl font-bold text-gray-900"><%= @total_games %></div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Physical</div>
<div class="text-3xl font-bold text-blue-600"><%= @physical_games %></div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Digital</div>
<div class="text-3xl font-bold text-green-600"><%= @digital_games %></div>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<div class="text-gray-500 text-sm">Completed</div>
<div class="text-3xl font-bold text-purple-600"><%= @completed_games %></div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Top Platforms -->
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-bold text-gray-900 mb-4">Top Platforms</h2>
<% if @games_by_platform.any? %>
<div class="space-y-3">
<% @games_by_platform.each do |platform_name, count| %>
<div class="flex justify-between items-center">
<span class="text-gray-700"><%= platform_name %></span>
<div class="flex items-center gap-2">
<div class="w-32 bg-gray-200 rounded-full h-2">
<div class="bg-indigo-600 h-2 rounded-full"
style="width: <%= (count.to_f / @total_games * 100).round %>%"></div>
</div>
<span class="text-sm font-semibold text-gray-900 w-8 text-right"><%= count %></span>
</div>
</div>
<% end %>
</div>
<% else %>
<p class="text-gray-500 text-sm">No games yet</p>
<% end %>
</div>
<!-- Collections -->
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-bold text-gray-900 mb-4">Collections</h2>
<% if @public_collections.any? %>
<div class="space-y-2">
<% @public_collections.each do |collection| %>
<%= link_to collection, class: "block p-3 rounded hover:bg-gray-50 transition" do %>
<div class="flex justify-between items-center">
<div>
<div class="font-semibold text-gray-900"><%= collection.name %></div>
<% if collection.description.present? %>
<div class="text-sm text-gray-600 truncate"><%= collection.description %></div>
<% end %>
</div>
<span class="text-sm text-gray-500"><%= collection.games.count %> games</span>
</div>
<% end %>
<% end %>
</div>
<% else %>
<p class="text-gray-500 text-sm">No collections yet</p>
<% end %>
</div>
</div>
<!-- Recent Games -->
<div class="bg-white rounded-lg shadow p-6 mt-6">
<h2 class="text-xl font-bold text-gray-900 mb-4">Recent Additions</h2>
<% if @recent_games.any? %>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<% @recent_games.each do |game| %>
<div class="border rounded-lg p-3 hover:shadow-md transition">
<% if game.igdb_game&.cover_url.present? %>
<%= image_tag game.igdb_game.cover_image_url("cover_big"),
alt: game.title,
class: "w-full h-40 object-cover rounded mb-2" %>
<% else %>
<div class="w-full h-40 bg-gray-200 rounded mb-2 flex items-center justify-center">
<span class="text-gray-400 text-xs">No Cover</span>
</div>
<% end %>
<h3 class="font-semibold text-sm text-gray-900 truncate" title="<%= game.title %>">
<%= game.title %>
</h3>
<p class="text-xs text-gray-600"><%= game.platform.name %></p>
<div class="mt-1">
<span class="inline-block px-2 py-0.5 text-xs rounded <%= game.physical? ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' %>">
<%= game.format.titleize %>
</span>
</div>
</div>
<% end %>
</div>
<% else %>
<p class="text-gray-500 text-sm">No games yet</p>
<% end %>
</div>
<!-- Footer Note -->
<% if current_user.nil? %>
<div class="mt-6 text-center text-sm text-gray-500">
<p>This is a public profile. <%= link_to "Create your own collection", signup_path, class: "text-indigo-600 hover:text-indigo-800" %></p>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,22 @@
{
"name": "TurbovaultWeb",
"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": "TurbovaultWeb.",
"theme_color": "red",
"background_color": "red"
}

View File

@@ -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)
// }
// })
// )
// })

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
<div class="max-w-md mx-auto">
<h1 class="text-3xl font-bold mb-6">Login</h1>
<%= form_with url: login_path, method: :post, class: "space-y-4" do |f| %>
<div>
<%= f.label :email, class: "block text-sm font-medium text-gray-700" %>
<%= f.email_field :email, autofocus: true, required: true, 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 :password, class: "block text-sm font-medium text-gray-700" %>
<%= f.password_field :password, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div>
<%= f.submit "Login", class: "w-full px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700" %>
</div>
<% end %>
<div class="mt-4 text-center space-y-2">
<%= link_to "Forgot your password?", new_password_reset_path, class: "text-indigo-600 hover:text-indigo-800" %>
<p class="text-gray-600">
Don't have an account? <%= link_to "Sign up", signup_path, class: "text-indigo-600 hover:text-indigo-800" %>
</p>
</div>
<% if Rails.env.development? %>
<div class="mt-6 p-4 bg-green-50 border-l-4 border-green-400 rounded">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-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-green-700">
<strong>Development Mode - Demo Account:</strong><br>
Email: <code class="bg-green-100 px-2 py-1 rounded">demo@turbovault.com</code><br>
Password: <code class="bg-green-100 px-2 py-1 rounded">password123</code><br>
<span class="text-xs mt-1 block">Includes 12 sample games with collections!</span>
</p>
</div>
</div>
</div>
<% end %>
</div>

View File

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

View File

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

View File

@@ -0,0 +1,45 @@
<div class="max-w-md mx-auto">
<h1 class="text-3xl font-bold mb-6">Sign Up</h1>
<%= form_with model: @user, url: users_path, class: "space-y-4" do |f| %>
<% if @user.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<ul>
<% @user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :username, class: "block text-sm font-medium text-gray-700" %>
<%= f.text_field :username, autofocus: true, required: true, 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 :email, class: "block text-sm font-medium text-gray-700" %>
<%= f.email_field :email, required: true, 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 :password, class: "block text-sm font-medium text-gray-700" %>
<%= f.password_field :password, required: true, 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 :password_confirmation, class: "block text-sm font-medium text-gray-700" %>
<%= f.password_field :password_confirmation, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div>
<%= f.submit "Sign Up", class: "w-full px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700" %>
</div>
<% end %>
<div class="mt-4 text-center">
<p class="text-gray-600">
Already have an account? <%= link_to "Login", login_path, class: "text-indigo-600 hover:text-indigo-800" %>
</p>
</div>
</div>

View File

@@ -0,0 +1,110 @@
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Settings</h1>
<div class="bg-white p-6 rounded-lg shadow mb-6">
<h2 class="text-xl font-bold mb-4">Profile Settings</h2>
<%= form_with model: @user, url: user_path(@user), method: :patch, class: "space-y-4" do |f| %>
<% if @user.errors.any? %>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
<ul>
<% @user.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= f.label :username, class: "block text-sm font-medium text-gray-700" %>
<%= f.text_field :username, 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 :email, class: "block text-sm font-medium text-gray-700" %>
<%= f.email_field :email, 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 :bio, class: "block text-sm font-medium text-gray-700" %>
<%= f.text_area :bio, rows: 4, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div class="flex items-center">
<%= f.check_box :profile_public, class: "rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" %>
<%= f.label :profile_public, "Make my profile public", class: "ml-2 text-sm text-gray-700" %>
</div>
<div class="border-t pt-4">
<%= f.label :theme, "Theme", class: "block text-sm font-medium text-gray-700 mb-2" %>
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<% {
"light" => { name: "Light", colors: "bg-white border-gray-300", icon: "☀️" },
"dark" => { name: "Dark", colors: "bg-gray-900 text-white border-gray-600", icon: "🌙" },
"midnight" => { name: "Midnight", colors: "bg-blue-950 text-blue-100 border-blue-800", icon: "🌃" },
"retro" => { name: "Retro", colors: "bg-amber-950 text-amber-100 border-amber-700", icon: "🕹️" },
"ocean" => { name: "Ocean", colors: "bg-cyan-950 text-cyan-100 border-cyan-700", icon: "🌊" }
}.each do |value, theme_info| %>
<label class="relative cursor-pointer">
<%= f.radio_button :theme, value, class: "peer sr-only" %>
<div class="<%= theme_info[:colors] %> border-2 peer-checked:border-indigo-600 peer-checked:ring-2 peer-checked:ring-indigo-600 rounded-lg p-4 text-center transition hover:scale-105">
<div class="text-3xl mb-2"><%= theme_info[:icon] %></div>
<div class="font-semibold text-sm"><%= theme_info[:name] %></div>
</div>
</label>
<% end %>
</div>
<p class="mt-2 text-xs text-gray-500">Choose your preferred theme for TurboVault</p>
</div>
<div class="border-t pt-4">
<div class="flex items-start">
<%= f.check_box :igdb_sync_enabled, class: "rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 mt-1" %>
<div class="ml-2">
<%= f.label :igdb_sync_enabled, "Enable IGDB game matching", class: "text-sm font-medium text-gray-700" %>
<p class="text-xs text-gray-500 mt-1">
<strong>Enabled by default.</strong> Automatically match your games with IGDB for cover art, genres, and metadata.
Sync runs every 30 minutes for unmatched games. You can review matches before they're linked.
</p>
</div>
</div>
</div>
<div>
<%= f.submit "Update Profile", class: "px-6 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700" %>
</div>
<% end %>
</div>
<div class="bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">API Tokens</h2>
<%= link_to "Manage API Tokens", settings_api_tokens_path, class: "text-indigo-600 hover:text-indigo-800" %>
</div>
<% if @api_tokens.any? %>
<div class="space-y-2">
<% @api_tokens.first(3).each do |token| %>
<div class="flex justify-between items-center py-2 border-b">
<div>
<div class="font-medium"><%= token.name || "Unnamed Token" %></div>
<div class="text-sm text-gray-500">
Created <%= time_ago_in_words(token.created_at) %> ago
<% if token.last_used_at %>
· Last used <%= time_ago_in_words(token.last_used_at) %> ago
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<% if @api_tokens.count > 3 %>
<div class="mt-4">
<%= link_to "View all #{@api_tokens.count} tokens", settings_api_tokens_path, class: "text-indigo-600 hover:text-indigo-800" %>
</div>
<% end %>
<% else %>
<p class="text-gray-500">No API tokens yet. Create one to access the API.</p>
<% end %>
</div>
</div>

View File

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