Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,32 +109,61 @@ def shop
end

# POST /shop/purchase
# Purchases a shop item. Requires authentication.
# Purchases a shop item or grant variant. Requires authentication.
# Supports both database-backed items (item_id) and virtual grant variants (category + variant).
def purchase
require_auth or return

item = ShopItem.where(status: [ "active", "in stock", "stock", nil, "" ]).find_by(id: params[:item_id])
# Try to find a database-backed item first
item = ShopItem.where(status: [ "active", "in stock", "stock", nil, "" ]).find_by(id: params[:item_id]) if params[:item_id].present?

unless item
redirect_to shop_path, flash: { error: "Item not found" }
return
if item
# Database-backed item purchase
cost = item.cost || 0
name = item.name
shop_item = item
else
# Virtual grant variant purchase (category + variant tier)
category = params[:category].to_s.strip
variant = params[:variant].to_s.strip.downcase
quantity = [ params[:quantity].to_i, 1 ].max

valid_categories = %w[keyboard mouse monitor headphones webcam]
bolts_map = { "standard" => 500, "quality" => 1100, "advanced" => 1700, "professional" => 2300 }
grant_map = { "standard" => 50, "quality" => 110, "advanced" => 170, "professional" => 230 }

unless valid_categories.include?(category.downcase) && bolts_map.key?(variant)
redirect_to shop_path, flash: { error: "Invalid item selection" }
return
end

cost = bolts_map[variant] * quantity
grant_amount = grant_map[variant] * quantity
name = "#{category.capitalize} #{variant.capitalize} Grant (#{quantity}x) - $#{grant_amount} HCB"

# Find or create a placeholder ShopItem for grant orders
shop_item = ShopItem.find_or_create_by!(name: "Grant Order Placeholder") do |si|
si.cost = 0
si.status = "system"
si.description = "System placeholder for grant-based orders"
end
end

if @current_user.balance < (item.cost || 0)
if @current_user.balance < cost
redirect_to shop_path, flash: { error: "Not enough bolts!" }
return
end

ActiveRecord::Base.transaction do
@current_user.update!(balance: @current_user.balance - item.cost)
@current_user.update!(balance: @current_user.balance - cost)
@current_user.shop_orders.create!(
shop_item: item,
name: item.name,
shop_item: shop_item,
name: name,
status: "pending"
)
end

redirect_to shop_path, flash: { success: "Purchased #{item.name}!" }
redirect_to shop_path, flash: { success: "Purchased #{name}!" }
rescue ActiveRecord::RecordInvalid => e
redirect_to shop_path, flash: { error: "Purchase failed: #{e.message}" }
end
Expand Down
2 changes: 2 additions & 0 deletions app/views/pages/shop.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@
</div>
<%= form_with url: purchase_path, method: :post, local: true, id: "shop-modal-form" do %>
<input type="hidden" name="item_id" id="shop-modal-item-id" value="">
<input type="hidden" name="category" id="shop-modal-category" value="">
<input type="hidden" name="variant" id="shop-modal-variant" value="">
<input type="hidden" name="quantity" id="shop-modal-quantity" value="1">
<button id="shop-modal-buy" type="submit" class="btn btn--primary btn--block">Purchase</button>
<% end %>
</div>
Expand Down
25 changes: 17 additions & 8 deletions public/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@
const modalShortage = document.getElementById("shop-modal-shortage");
const modalForm = document.getElementById("shop-modal-form");
const modalItemId = document.getElementById("shop-modal-item-id");
const modalCategory = document.getElementById("shop-modal-category");
const modalVariant = document.getElementById("shop-modal-variant");
const modalQuantity = document.getElementById("shop-modal-quantity");
const modalBuy = document.getElementById("shop-modal-buy");
const userBalance = Number(modal?.dataset.userBalance || 0);

Expand All @@ -109,12 +111,16 @@
const name = modal.dataset.itemName || "Item";
const qty = Math.max(1, Number(modalQty?.value || 1));
const totalBolts = baseBolts * qty;
const totalGrant = grant * qty;
if (modalPrice) modalPrice.textContent = String(totalBolts);
if (modalDesc) {
modalDesc.textContent = `This item provide a ${grant}$ HCB card grant to spend on a ${name}. Quantity: ${qty}.`;
modalDesc.textContent = `This provides a $${totalGrant} HCB card grant to spend on a ${name}. Quantity: ${qty}.`;
}
// enable/disable buy based on computed total bolts
const canBuy = Boolean(modalItemId?.value) && userBalance >= totalBolts;
// Sync quantity to hidden form field
if (modalQuantity) modalQuantity.value = String(qty);
// Enable buy if user has enough balance (category/variant are always set for grant items)
const hasValidSelection = Boolean(modalCategory?.value) || Boolean(modalItemId?.value);
const canBuy = hasValidSelection && userBalance >= totalBolts;
if (modalBuy) modalBuy.disabled = !canBuy;
if (modalWarning) {
const shortage = Math.max(0, totalBolts - userBalance);
Expand All @@ -129,16 +135,19 @@
const display = data.display || (data.name ? `${data.name}_${variantLower}` : 'Item');
modalTitle.textContent = display;
modalImage.src = data.img || "/images/signin/hackclub.svg";
// store computation inputs on modal dataset
// Store computation inputs on modal dataset
modal.dataset.baseBolts = String(Number(data.bolts || 0));
modal.dataset.grant = String(Number(data.grant || 0));
modal.dataset.itemName = data.name || "Item";
// reset qty to 1 each open
// Reset qty to 1 each open
if (modalQty) modalQty.value = "1";
// compute totals, description, and buy state
// Set hidden form fields
if (modalItemId) modalItemId.value = data.itemId || "";
if (modalCategory) modalCategory.value = data.name || "";
if (modalVariant) modalVariant.value = data.variant || "";
if (modalQuantity) modalQuantity.value = "1";
// Compute totals, description, and buy state
updateModalTotals();
modalItemId.value = data.itemId || "";
modalVariant.value = data.variant || "";
modal.classList.remove("modal--hidden");
}

Expand Down
Loading