Files
Example-TCG-Site/templates/store/card_list.html
Ryan Westfall 9040021d1b MASSIVE UPDATE:
bounty board feature

buyers to see bounty boards

seller profile page (like have theme chooser)

Have the game and set name be filters.

Add cards to vault manually

update card inventory add to have the autocomplete for the card  -

store analytics, clicks, views, link to store (url/QR code)

bulk item inventory creation --

Make the banner feature flag driven so I can have a beta site setup like the primary site

don't use primary key values in urls - update to use uuid4 values

site analytics. tianji is being sent

item potent on the mtg and lorcana populate scripts

Card item images for specific listings

check that when you buy a card it is in the vault

Buys should be able to search on store inventories

More pie charts for the seller!

post bounty board is slow to load

seller reviews/ratings - show a historgram - need a way for someone to rate

Report a seller feature for buyer to report

Make sure the stlying is consistent based on the theme choosen

smart minimum order quantity and shipping amounts (defined by the store itself)

put virtual packs behind a feature flag like bounty board

proxy service feature flag

Terms of Service

new description for TCGKof

store SSN, ITIN, and EIN

optomize for SEO
2026-01-23 12:28:20 -06:00

203 lines
11 KiB
HTML

{% extends 'base/layout.html' %}
{% load static %}
{% block content %}
<div class="browse-container" style="display: grid; grid-template-columns: 250px 1fr; gap: 2rem;">
<!-- Sidebar Filters -->
<aside class="browse-sidebar"
style="background: var(--card-bg); padding: 1.5rem; border-radius: 0.5rem; border: 1px solid var(--border-color); height: fit-content;">
<h3 style="margin-top: 0;">Filters</h3>
<form method="get">
<div style="margin-bottom: 1rem;">
<label style="display: block; font-size: 0.875rem; margin-bottom: 0.5rem;">Game</label>
<select name="game"
style="width: 100%; padding: 0.5rem; border-radius: 0.25rem; background: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color);"
onchange="this.form.submit()">
<option value="">All Games</option>
{% for game in games %}
<option value="{{ game.slug }}" {% if current_game|slugify == game.slug %}selected{% endif %}>
{{game.name}}
</option>
{% endfor %}
</select>
</div>
{% if current_game %}
<div style="margin-bottom: 1rem;">
<label style="display: block; font-size: 0.875rem; margin-bottom: 0.5rem;">Set</label>
<select name="set"
style="width: 100%; padding: 0.5rem; border-radius: 0.25rem; background: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color);">
<option value="">All Sets</option>
{% for set in sets %}
<option value="{{ set.id }}" {% if request.GET.set|add:"0" == set.id %}selected{% endif %}>{{ set.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
<div style="margin-bottom: 1rem; display: flex; align-items: center;">
<input type="checkbox" id="hide_out_of_stock" name="hide_out_of_stock" {% if hide_oos == 'on' %}checked{% endif %} onchange="this.form.submit()" style="margin-right: 0.5rem;">
<label for="hide_out_of_stock" style="font-size: 0.875rem; cursor: pointer;">Hide Out of Stock</label>
</div>
<div style="margin-bottom: 1rem; position: relative;">
<label style="display: block; font-size: 0.875rem; margin-bottom: 0.5rem;">Search</label>
<input type="text" name="q" id="search-input" value="{{ search_query|default:'' }}" placeholder="Card name..." autocomplete="off"
style="width: 90%; padding: 0.5rem; border-radius: 0.25rem; background: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color);">
<ul id="suggestions-list" style="display: none; position: absolute; top: 100%; left: 0; width: 90%; background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 0.25rem; z-index: 1000; list-style: none; padding: 0; margin: 0; max-height: 200px; overflow-y: auto;">
</ul>
</div>
<script>
const searchInput = document.getElementById('search-input');
const suggestionsList = document.getElementById('suggestions-list');
let debounceTimer;
searchInput.addEventListener('input', function() {
const query = this.value;
clearTimeout(debounceTimer);
if (query.length < 2) {
suggestionsList.style.display = 'none';
return;
}
debounceTimer = setTimeout(() => {
fetch(`/api/card-autocomplete/?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
suggestionsList.innerHTML = '';
if (data.results.length > 0) {
data.results.forEach(name => {
const li = document.createElement('li');
li.textContent = name;
li.style.padding = '0.5rem';
li.style.cursor = 'pointer';
li.style.borderBottom = '1px solid var(--border-color)';
li.addEventListener('mouseenter', () => {
li.style.background = 'var(--card-bg)';
});
li.addEventListener('mouseleave', () => {
li.style.background = 'transparent';
});
li.addEventListener('click', () => {
searchInput.value = name;
suggestionsList.style.display = 'none';
searchInput.form.submit();
});
suggestionsList.appendChild(li);
});
suggestionsList.style.display = 'block';
} else {
suggestionsList.style.display = 'none';
}
});
}, 300);
});
document.addEventListener('click', function(e) {
if (e.target !== searchInput && e.target !== suggestionsList) {
suggestionsList.style.display = 'none';
}
});
</script>
<button type="submit" class="btn" style="width: 100%;">Apply Filters</button>
<a href="{% url 'store:card_list' %}"
style="display: block; text-align: center; margin-top: 1rem; color: #94a3b8; font-size: 0.875rem; text-decoration: none;">Clear
Filters</a>
</form>
</aside>
<!-- Card Grid -->
<div>
<h2 style="margin-top: 0;">Browse Cards</h2>
<div class="card-grid">
{% for card in page_obj %}
<div class="tcg-card">
<a href="{% url 'store:card_detail' card.uuid %}" style="text-decoration: none; color: inherit;">
<div style="aspect-ratio: 2.5/3.5; background: #000; position: relative;">
<!-- Placeholder or Real Image -->
{% if card.image_url %}
<img src="{{ card.image_url }}" alt="{{ card.name }}"
style="width: 100%; height: 100%; object-fit: cover;">
{% else %}
<div
style="display: flex; align-items: center; justify-content: center; height: 100%; color: #64748b; background: #334155;">
No Image</div>
{% endif %}
</div>
<div class="tcg-card-body">
<h4
style="margin: 0 0 0.5rem; font-size: 1rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ card.name }}</h4>
<div
style="display: flex; justify-content: space-between; align-items: center; font-size: 0.875rem; color: #94a3b8;">
<span>{{ card.set.code|default:card.set.game.name }}</span>
<span>{{ card.rarity }}</span>
</div>
<div
style="margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center;">
{% with card.listings.first as cheapest %}
{% if cheapest %}
<span style="font-weight: 700; color: var(--text-color);">From ${{ cheapest.price }}</span>
{% else %}
<span style="color: #64748b;">Out of Stock</span>
{% endif %}
{% endwith %}
<span id="stock-{{ card.uuid }}" class="stock-counter" data-card-id="{{ card.uuid }}" style="font-size: 0.75rem; color: #94a3b8; margin-left: auto;">...</span>
</div>
</div>
</a>
</div>
{% empty %}
<p>No cards found matching your criteria.</p>
{% endfor %}
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<div style="margin-top: 2rem; display: flex; justify-content: center; gap: 0.5rem;">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}&game={{ current_game }}&q={{ search_query }}&hide_out_of_stock={{ hide_oos }}" class="btn"
style="padding: 0.25rem 0.5rem; font-size: 0.875rem;">Prev</a>
{% endif %}
<span
style="padding: 0.25rem 0.75rem; background: var(--card-bg); border-radius: 0.25rem; border: 1px solid var(--border-color);">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&game={{ current_game }}&q={{ search_query }}&hide_out_of_stock={{ hide_oos }}" class="btn"
style="padding: 0.25rem 0.5rem; font-size: 0.875rem;">Next</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const stockCounters = document.querySelectorAll('.stock-counter');
stockCounters.forEach(counter => {
const cardId = counter.getAttribute('data-card-id');
fetch(`/store/api/stock/${cardId}/`)
.then(response => response.json())
.then(data => {
if (data.total_stock > 0) {
counter.textContent = `${data.total_stock} in stock`;
counter.style.color = '#10b981'; // green
} else {
counter.textContent = 'Out of Stock';
counter.style.color = '#ef4444'; // red
}
})
.catch(err => {
counter.textContent = 'Stock unknown';
});
});
});
</script>
{% endblock %}