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
This commit is contained in:
2026-01-23 12:28:20 -06:00
parent c43603bfb5
commit 9040021d1b
80 changed files with 6938 additions and 592 deletions

View File

@@ -0,0 +1,164 @@
{% 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;">Filter Bounties</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 == 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; 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 or Bounty title..." 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>
document.addEventListener("DOMContentLoaded", function() {
const searchInput = document.getElementById('search-input');
const suggestionsList = document.getElementById('suggestions-list');
let debounceTimer;
if (searchInput) {
searchInput.addEventListener('input', function() {
const query = this.value;
clearTimeout(debounceTimer);
if (query.length < 2) {
suggestionsList.style.display = 'none';
return;
}
debounceTimer = setTimeout(() => {
fetch(`/api/bounty-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:bounty_list' %}"
style="display: block; text-align: center; margin-top: 1rem; color: #94a3b8; font-size: 0.875rem; text-decoration: none;">Clear
Filters</a>
</form>
</aside>
<!-- Bounty Grid -->
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Bounty Board</h2>
{% if user.seller_profile %}
<a href="{% url 'store:bounty_create' %}" class="btn btn-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;">Post a Bounty</a>
{% endif %}
</div>
<div class="card-grid">
{% for bounty in bounties %}
<div class="tcg-card">
<a href="{% url 'store:bounty_detail' bounty.uuid %}" style="text-decoration: none; color: inherit;">
<div style="aspect-ratio: 2.5/3.5; background: #000; position: relative;">
{% if bounty.card.image_url %}
<img src="{{ bounty.card.image_url }}" alt="{{ bounty.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; padding: 1rem; text-align: center;">
{{ bounty.title|default:"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;">
{% if bounty.card %}{{ bounty.card.name }}{% else %}{{ bounty.title }}{% endif %}
</h4>
<div
style="display: flex; justify-content: space-between; align-items: center; font-size: 0.875rem; color: #94a3b8;">
<span>{% if bounty.card %}{{ bounty.card.set.code|default:bounty.card.set.game.name }}{% else %}Wanted{% endif %}</span>
<span style="color: #10b981;">Offering ${{ bounty.target_price }}</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;">
<span style="font-size: 0.75rem; color: #94a3b8;">{{ bounty.offers.count }} Offers</span>
<span class="btn" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">View Bounty</span>
</div>
</div>
</a>
</div>
{% empty %}
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; background: var(--card-bg); border-radius: 0.5rem; border: 1px solid var(--border-color);">
<p style="color: #94a3b8; font-size: 1.1rem; margin-bottom: 1rem;">No active bounties found matching your criteria.</p>
<a href="{% url 'store:bounty_list' %}" class="btn btn-outline">Clear Filters</a>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,16 +1,52 @@
{% extends 'base/layout.html' %}
{% block title %}{{ card.name }} - TCGKof{% endblock %}
{% block meta_description %}Buy {{ card.name }} ({{ card.set.name }}) on TCGKof. {{ listings|length }} listings available starting from ${{ listings.first.price|default:'0.00' }}.{% endblock %}
{% block meta_keywords %}{{ card.name }}, {{ card.set.name }}, {{ card.rarity }}, {{ card.set.game.name }}, buy {{ card.name }}, sell {{ card.name }}{% endblock %}
{% block og_title %}{{ card.name }} - {{ card.set.name }} | TCGKof{% endblock %}
{% block og_description %}Buy {{ card.name }} from {{ card.set.name }} set. best prices on TCGKof.{% endblock %}
{% block og_image %}{{ card.image_url|default:'' }}{% endblock %}
{% block og_type %}product{% endblock %}
{% block twitter_title %}{{ card.name }} - {{ card.set.name }}{% endblock %}
{% block twitter_description %}Find the best deals for {{ card.name }} on TCGKof.{% endblock %}
{% block twitter_image %}{{ card.image_url|default:'' }}{% endblock %}
{% block content %}
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "Product",
"name": "{{ card.name }}",
"image": "{{ card.image_url }}",
"description": "Buy {{ card.name }} from the {{ card.set.name }} set.",
"sku": "{{ card.uuid }}",
"brand": {
"@type": "Brand",
"name": "{{ card.set.game.name }}"
},
"offers": {
"@type": "AggregateOffer",
"url": "{{ request.build_absolute_uri }}",
"priceCurrency": "USD",
"lowPrice": "{{ listings.first.price|default:'0.00' }}",
"offerCount": "{{ listings|length }}",
"availability": "https://schema.org/InStock"
}
}
</script>
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 3rem;">
<!-- Image Column -->
<div>
<div
style="background: #000; border-radius: 0.75rem; overflow: hidden; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);">
style="background: var(--card-bg); border-radius: 0.75rem; overflow: hidden; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);">
{% if card.image_url %}
<img src="{{ card.image_url }}" alt="{{ card.name }}" style="width: 100%; display: block;">
{% else %}
<div
style="aspect-ratio: 2.5/3.5; display: flex; align-items: center; justify-content: center; color: #64748b; background: #334155;">
style="aspect-ratio: 2.5/3.5; display: flex; align-items: center; justify-content: center; color: var(--muted-text-color); background: var(--border-color);">
No Image</div>
{% endif %}
</div>
@@ -19,13 +55,13 @@
<div style="margin-top: 1.5rem; display: grid; gap: 0.5rem;">
{% if card.tcgplayer_id %}
<a href="https://www.tcgplayer.com/product/{{ card.tcgplayer_id }}" target="_blank"
style="display: block; text-align: center; padding: 0.75rem; background: #27272a; color: white; border-radius: 0.375rem; text-decoration: none; font-size: 0.875rem;">
style="display: block; text-align: center; padding: 0.75rem; background: var(--card-bg); color: var(--text-color); border-radius: 0.375rem; text-decoration: none; font-size: 0.875rem;">
View on TCGPlayer
</a>
{% endif %}
<a href="https://www.ebay.com/sch/i.html?_nkw={{ card.name|urlencode }}+{{ card.set.name|urlencode }}"
target="_blank"
style="display: block; text-align: center; padding: 0.75rem; background: #27272a; color: white; border-radius: 0.375rem; text-decoration: none; font-size: 0.875rem;">
style="display: block; text-align: center; padding: 0.75rem; background: var(--card-bg); color: var(--text-color); border-radius: 0.375rem; text-decoration: none; font-size: 0.875rem;">
Search on eBay
</a>
</div>
@@ -36,7 +72,7 @@
<div style="margin-bottom: 2rem;">
<h4 style="margin: 0; color: var(--primary-color);">{{ card.set.game.name }} &bull; {{ card.set.name }}</h4>
<h1 style="margin: 0.5rem 0 0; font-size: 2.5rem;">{{ card.name }}</h1>
<div style="margin-top: 1rem; display: flex; gap: 1rem; color: #94a3b8;">
<div style="margin-top: 1rem; display: flex; gap: 1rem; color: var(--muted-text-color);">
<span>Rarity: <strong style="color: var(--text-color);">{{ card.rarity }}</strong></span>
<span>Collector #: <strong style="color: var(--text-color);">{{ card.collector_number }}</strong></span>
</div>
@@ -46,7 +82,7 @@
Listings</h3>
<div style="margin-bottom: 1rem;">
<label style="font-size: 0.875rem; color: #94a3b8;">Filter Condition:</label>
<label style="font-size: 0.875rem; color: var(--muted-text-color);">Filter Condition:</label>
<div style="display: flex; gap: 0.5rem; margin-top: 0.25rem;">
<button onclick="filterCondition('ALL')" class="btn" style="padding: 0.25rem 0.75rem; font-size: 0.75rem; background: var(--card-bg); border: 1px solid var(--border-color);">All</button>
<button onclick="filterCondition('NM')" class="btn" style="padding: 0.25rem 0.75rem; font-size: 0.75rem; background: var(--card-bg); border: 1px solid var(--border-color);">NM</button>
@@ -61,30 +97,43 @@
<div class="listing-item" data-condition="{{ listing.condition }}"
style="display: flex; justify-content: space-between; align-items: center; background: var(--card-bg); padding: 1rem; border-radius: 0.5rem; border: 1px solid var(--border-color);">
<div style="display: flex; gap: 1rem; align-items: center;">
<span style="font-weight: 700; font-size: 1.25rem; width: 3rem; text-align: center;">{{
listing.condition }}</span>
{% if listing.image %}
<img src="{{ listing.image.url }}" alt="Listing Image" style="width: 50px; height: 70px; object-fit: cover; border-radius: 4px; border: 1px solid #444;">
{% endif %}
<span style="font-weight: 700; font-size: 1.25rem;">
{{ listing.condition }}</span>
<div>
<div style="font-size: 0.875rem; color: #94a3b8;">Condition</div>
<div style="font-size: 0.875rem; color: var(--muted-text-color);">Condition</div>
{% if listing.is_foil %}
<span
style="background: linear-gradient(45deg, #f59e0b, #d97706); -webkit-background-clip: text; color: transparent; font-weight: 700; font-size: 0.75rem; text-transform: uppercase;">Foil</span>
style="background: linear-gradient(45deg, #f59e0b, #d97706); -webkit-background-clip: text; background-clip: text; color: transparent; font-weight: 700; font-size: 0.75rem; text-transform: uppercase;">Foil</span>
{% endif %}
</div>
</div>
<div style="display: flex; align-items: center; gap: 2rem;">
<div style="min-width: 150px;">
<div style="font-size: 0.875rem; color: var(--muted-text-color);">Seller</div>
<div style="font-weight: 500;">
{% if listing.seller %}
<a href="{% url 'store:seller_profile' listing.seller.slug %}" style="color: var(--info-color); text-decoration: none;">{{ listing.seller.store_name }}</a>
{% else %}
<span style="color: var(--muted-text-color);">TCGKof Direct</span>
{% endif %}
</div>
</div>
<div style="text-align: right;">
<div style="font-weight: 700; font-size: 1.5rem;">${{ listing.price }}</div>
<div style="font-size: 0.75rem; color: #94a3b8;">{{ listing.quantity }} available</div>
<div style="font-size: 0.75rem; color: var(--muted-text-color);">{{ listing.quantity }} available</div>
</div>
{% if user.is_authenticated %}
<form action="{% url 'store:add_to_cart' listing.id %}" method="post">
<form action="{% url 'store:add_to_cart' listing.uuid %}" method="post">
{% csrf_token %}
<button type="submit" class="btn">Add to Cart</button>
</form>
{% else %}
<a href="{% url 'login' %}?next={{ request.path }}" class="btn" style="background: #334155;">Login
<a href="{% url 'login' %}?next={{ request.path }}" class="btn" style="background: var(--border-color);">Login
to Buy</a>
{% endif %}
</div>
@@ -92,21 +141,23 @@
{% empty %}
<div
style="text-align: center; padding: 2rem; background: var(--card-bg); border-radius: 0.5rem; border: 1px dashed var(--border-color);">
<p style="margin: 0; color: #94a3b8;">No listings currently available for this card.</p>
<p style="margin: 0; color: var(--muted-text-color);">No listings currently available for this card.</p>
</div>
{% endfor %}
</div>
<!-- Proxy Service -->
{% if FEATURE_PLAYTEST_PROXY %}
<div style="margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--border-color);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<h3 style="margin: 0 0 0.5rem;">Playtest Proxy Service</h3>
<p style="margin: 0; color: #94a3b8; font-size: 0.875rem;">Download a high-res proxy for playtesting. Credit offered if you buy later.</p>
<p style="margin: 0; color: var(--muted-text-color); font-size: 0.875rem;">Download a high-res proxy for playtesting. Credit offered if you buy later.</p>
</div>
<button onclick="alert('Proxy PDF generated! (Mockup)')" class="btn" style="background: transparent; border: 1px solid var(--border-color);">Download Proxy</button>
</div>
</div>
{% endif %}
<script>
function filterCondition(cond) {

View File

@@ -2,9 +2,9 @@
{% load static %}
{% block content %}
<div style="display: grid; grid-template-columns: 250px 1fr; gap: 2rem;">
<div class="browse-container" style="display: grid; grid-template-columns: 250px 1fr; gap: 2rem;">
<!-- Sidebar Filters -->
<aside
<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">
@@ -29,19 +29,80 @@
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>
<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;">
<label style="display: block; font-size: 0.875rem; margin-bottom: 0.5rem;">Search</label>
<input type="text" name="q" value="{{ search_query|default:'' }}" placeholder="Card name..."
style="width: 90%; padding: 0.5rem; border-radius: 0.25rem; background: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color);">
<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
@@ -55,7 +116,7 @@
<div class="card-grid">
{% for card in page_obj %}
<div class="tcg-card">
<a href="{% url 'store:card_detail' card.id %}" style="text-decoration: none; color: inherit;">
<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 %}
@@ -85,7 +146,7 @@
<span style="color: #64748b;">Out of Stock</span>
{% endif %}
{% endwith %}
<span id="stock-{{ card.id }}" class="stock-counter" data-card-id="{{ card.id }}" style="font-size: 0.75rem; color: #94a3b8; margin-left: auto;">...</span>
<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>
@@ -99,7 +160,7 @@
{% 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 }}" class="btn"
<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 %}
@@ -109,7 +170,7 @@
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&game={{ current_game }}&q={{ search_query }}" class="btn"
<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>

View File

@@ -7,7 +7,19 @@
{% if cart and cart.items.count > 0 %}
<div
style="background: var(--card-bg); border-radius: 0.5rem; border: 1px solid var(--border-color); overflow: hidden;">
{% for item in cart.items.all %}
<div
style="background: var(--card-bg); border-radius: 0.5rem; border: 1px solid var(--border-color); overflow: hidden;">
{% for section in cart_data %}
<div style="background: rgba(0,0,0,0.05); padding: 0.75rem 1.5rem; border-bottom: 1px solid var(--border-color); font-weight: 600;">
{% if section.seller %}
Store: <a href="{% url 'store:seller_profile' section.seller.slug %}" style="text-decoration: none; color: inherit;">{{ section.seller.store_name }}</a>
{% else %}
System Items
{% endif %}
</div>
{% for item in section.items %}
<div
style="display: flex; justify-content: space-between; align-items: center; padding: 1.5rem; border-bottom: 1px solid var(--border-color);">
<div style="display: flex; gap: 1.5rem; align-items: center;">
@@ -43,12 +55,27 @@
<div style="font-weight: 700; font-size: 1.25rem;">
${{ item.total_price }}
</div>
<a href="{% url 'store:remove_from_cart' item.id %}"
<a href="{% url 'store:remove_from_cart' item.uuid %}"
style="color: #ef4444; text-decoration: none; font-size: 1.25rem;">&times;</a>
</div>
</div>
{% endfor %}
<div style="padding: 1rem 1.5rem; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.02);">
<div>
{% if section.free_shipping_needed > 0 %}
<span style="color: #ef4444; font-size: 0.875rem;">Add ${{ section.free_shipping_needed }} more for free shipping!</span>
{% else %}
<span style="color: #10b981; font-size: 0.875rem;">Free Shipping Qualifies!</span>
{% endif %}
</div>
<div style="text-align: right; font-size: 0.875rem;">
<div>Subtotal: ${{ section.subtotal }}</div>
<div>Shipping: ${{ section.shipping_cost }}</div>
</div>
</div>
{% endfor %}
<div style="padding: 1rem 1.5rem; background: rgba(0,0,0,0.1); border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<a href="{% url 'store:toggle_insurance' %}" style="text-decoration: none; display: flex; align-items: center; gap: 0.5rem; color: var(--text-color);">
@@ -63,7 +90,7 @@
<div
style="padding: 1.5rem; background: rgba(0,0,0,0.2); display: flex; justify-content: space-between; align-items: center;">
<span style="font-size: 1.25rem; font-weight: 600;">Total</span>
<span style="font-size: 1.5rem; font-weight: 800;">${{ cart.total_price }}</span>
<span style="font-size: 1.5rem; font-weight: 800;">${{ grand_total }}</span>
</div>
</div>

View File

@@ -15,7 +15,7 @@
</div>
<div class="tcg-card-body" style="text-align: center;">
<h4 style="margin: 0 0 0.5rem; font-size: 1rem;">{{ pack.listing.name }}</h4>
<a href="{% url 'store:open_pack' pack.id %}" class="btn" style="width: 100%; display: block; margin-top: 1rem;">Open Pack</a>
<a href="{% url 'store:open_pack' pack.uuid %}" class="btn" style="width: 100%; display: block; margin-top: 1rem;">Open Pack</a>
</div>
</div>
{% endfor %}

View File

@@ -70,5 +70,85 @@
</div>
{% endfor %}
</div>
<!-- Rating Section -->
{% if order.seller %}
<div style="background: var(--card-bg); padding: 2rem; border-radius: 0.5rem; border: 1px solid var(--border-color); margin-top: 2rem;">
<h3 style="margin-bottom: 1rem;">Rate Your Order</h3>
{% if order.rating %}
<div style="display: flex; align-items: center; gap: 0.5rem;">
<p style="margin: 0; color: #94a3b8;">You rated this order:</p>
<div style="display: flex; gap: 0.25rem;">
{% for i in "12345" %}
{% if forloop.counter <= order.rating %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#fbbf24" stroke="#fbbf24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
{% else %}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
{% endif %}
{% endfor %}
</div>
<p style="margin: 0; font-weight: 600;">{{ order.rating }}/5</p>
</div>
{% else %}
<p style="color: #94a3b8; margin-bottom: 1rem;">How would you rate your experience with {{ order.seller.store_name }}?</p>
<form method="post" style="display: flex; gap: 1rem; align-items: center;">
{% csrf_token %}
<div style="display: flex; gap: 0.5rem;" id="star-rating">
{% for i in "12345" %}
<label style="cursor: pointer;">
<input type="radio" name="rating" value="{{ forloop.counter }}" style="display: none;" class="rating-input">
<svg class="star-icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transition: all 0.2s;">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
</label>
{% endfor %}
</div>
<button type="submit" class="btn" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; opacity: 0.5; pointer-events: none;" id="submit-rating" disabled>Submit Rating</button>
</form>
<script>
const stars = document.querySelectorAll('.star-icon');
const ratingInputs = document.querySelectorAll('.rating-input');
const submitBtn = document.getElementById('submit-rating');
let selectedRating = 0;
stars.forEach((star, index) => {
star.addEventListener('mouseenter', () => {
highlightStars(index + 1);
});
star.addEventListener('click', () => {
selectedRating = index + 1;
ratingInputs[index].checked = true;
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
submitBtn.style.pointerEvents = 'auto';
});
});
document.getElementById('star-rating').addEventListener('mouseleave', () => {
highlightStars(selectedRating);
});
function highlightStars(count) {
stars.forEach((star, index) => {
if (index < count) {
star.setAttribute('fill', '#fbbf24');
star.setAttribute('stroke', '#fbbf24');
} else {
star.setAttribute('fill', 'none');
star.setAttribute('stroke', '#64748b');
}
});
}
</script>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -18,9 +18,17 @@
</div>
<div class="tcg-card-body">
<h4 style="margin: 0 0 0.5rem; font-size: 1rem;">{{ pack.name }}</h4>
<div style="font-size: 0.75rem; color: #94a3b8; margin-bottom: 0.5rem;">
Sold by:
{% if pack.seller %}
<a href="{% url 'store:seller_profile' pack.seller.slug %}" style="color: #60a5fa;">{{ pack.seller.store_name }}</a>
{% else %}
TCGKof Direct
{% endif %}
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem;">
<span style="font-weight: 700;">${{ pack.price }}</span>
<a href="{% url 'store:add_pack_to_cart' pack.id %}" class="btn" style="padding: 0.25rem 0.75rem; font-size: 0.875rem;">Add to Cart</a>
<a href="{% url 'store:add_pack_to_cart' pack.uuid %}" class="btn" style="padding: 0.25rem 0.75rem; font-size: 0.875rem;">Add to Cart</a>
</div>
</div>
</div>