Template
Add shop, POS sync, event ticketing, and shipping catalog apps.
Clients can run in-house retail without Shopify while keeping the same Docker/Django/Postgres stack and data-ownership promise. Closes #5.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Checkout cancelled{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg"><div class="container">
|
||||
<h1>Checkout cancelled</h1>
|
||||
<p>Order {{ order.number }} was not paid. You can return to the <a href="{% url 'shop:cart' %}">cart</a>.</p>
|
||||
</div></section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Cart · {{ SITE_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<h1>Cart</h1>
|
||||
{% for line in lines %}
|
||||
<p>
|
||||
<a href="{{ line.product.get_absolute_url }}">{{ line.product.name }}</a>
|
||||
· {{ line.quantity }} × {{ line.unit_price }} = {{ line.line_total }}
|
||||
</p>
|
||||
<form method="post" action="{% url 'shop:cart_update' line.product.slug %}" style="margin:0 0 16px">
|
||||
{% csrf_token %}
|
||||
<input name="quantity" type="number" min="0" value="{{ line.quantity }}">
|
||||
<button type="submit">Update</button>
|
||||
</form>
|
||||
{% empty %}
|
||||
<p>Cart is empty.</p>
|
||||
{% endfor %}
|
||||
{% if lines %}
|
||||
<p><strong>Total:</strong> {{ total }}</p>
|
||||
<p><a class="button button-primary" href="{% url 'shop:checkout' %}">Checkout</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Checkout · {{ SITE_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<h1>Checkout</h1>
|
||||
<p>Total: {{ total }}</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p><label>Email <input name="email" type="email" required></label></p>
|
||||
<p><label>Name <input name="customer_name"></label></p>
|
||||
<p><label>Address <input name="address_line1"></label></p>
|
||||
<p><label>Address 2 <input name="address_line2"></label></p>
|
||||
<p><label>City <input name="address_city"></label></p>
|
||||
<p><label>State <input name="address_state"></label></p>
|
||||
<p><label>ZIP <input name="address_zip"></label></p>
|
||||
<button class="button button-primary" type="submit">Pay with Stripe</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ product.name }} · {{ SITE_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<p><a href="{% url 'shop:list' %}">← Shop</a></p>
|
||||
<h1>{{ product.name }}</h1>
|
||||
<p class="muted">{{ product.price }} {{ product.currency|upper }} · {{ product.sku }}</p>
|
||||
<div>{{ product.description|linebreaks }}</div>
|
||||
{% if available is not None %}
|
||||
<p>{{ available }} in stock</p>
|
||||
{% endif %}
|
||||
<form method="post" action="{% url 'shop:cart_add' product.slug %}">
|
||||
{% csrf_token %}
|
||||
<label>Qty <input name="quantity" type="number" min="1" value="1"></label>
|
||||
<button class="button button-primary" type="submit">Add to cart</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Shop · {{ SITE_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg">
|
||||
<div class="container">
|
||||
<h1 class="text-uppercase">Shop</h1>
|
||||
<p><a href="{% url 'shop:cart' %}">View cart</a></p>
|
||||
{% for product in products %}
|
||||
<article style="margin:0 0 32px">
|
||||
<h2><a href="{{ product.get_absolute_url }}">{{ product.name }}</a></h2>
|
||||
<p class="muted">{{ product.price }} {{ product.currency|upper }} · {{ product.sku }}</p>
|
||||
<p>{{ product.description|truncatewords:40 }}</p>
|
||||
</article>
|
||||
{% empty %}
|
||||
<p>No products yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "portal_base.html" %}
|
||||
{% block title %}{{ order.number }} · Portal{% endblock %}
|
||||
{% block topbar_title %}{{ order.number }}{% endblock %}
|
||||
{% block portal_content %}
|
||||
<p><strong>{{ order.email }}</strong> · {{ order.amount }} {{ order.currency|upper }} · {{ order.get_status_display }}</p>
|
||||
{% if order.customer_name %}<p>{{ order.customer_name }}</p>{% endif %}
|
||||
<table class="table">
|
||||
<thead><tr><th>Item</th><th>SKU</th><th>Qty</th><th>Price</th></tr></thead>
|
||||
<tbody>
|
||||
{% for item in order.items.all %}
|
||||
<tr>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.sku }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ item.unit_price }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p><a href="{% url 'shop_portal:order_list' %}">← All orders</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "portal_base.html" %}
|
||||
{% block title %}Orders · Portal{% endblock %}
|
||||
{% block topbar_title %}Orders{% endblock %}
|
||||
{% block portal_content %}
|
||||
<table class="table">
|
||||
<thead><tr><th>Number</th><th>Email</th><th>Amount</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td><a href="{% url 'shop_portal:order_detail' order.pk %}">{{ order.number }}</a></td>
|
||||
<td>{{ order.email }}</td>
|
||||
<td>{{ order.amount }} {{ order.currency|upper }}</td>
|
||||
<td>{{ order.get_status_display }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4" class="empty-state">No orders yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "portal_base.html" %}
|
||||
{% block title %}{% if product %}Edit{% else %}New{% endif %} product · Portal{% endblock %}
|
||||
{% block topbar_title %}{% if product %}Edit product{% else %}New product{% endif %}{% endblock %}
|
||||
{% block portal_content %}
|
||||
<form method="post" class="form-grid">
|
||||
{% csrf_token %}
|
||||
<div class="field"><label>Name</label><input name="name" required value="{{ product.name|default:'' }}"></div>
|
||||
<div class="field"><label>SKU</label><input name="sku" value="{{ product.sku|default:'' }}" placeholder="auto from name"></div>
|
||||
<div class="field"><label>Slug</label><input name="slug" value="{{ product.slug|default:'' }}" placeholder="auto from name"></div>
|
||||
<div class="field"><label>Price</label><input name="price" type="number" step="0.01" min="0" required value="{{ product.price|default:'' }}"></div>
|
||||
<div class="field">
|
||||
<label>Fulfillment</label>
|
||||
<select name="fulfillment">
|
||||
<option value="stocked" {% if product.fulfillment == 'stocked' or not product %}selected{% endif %}>On-hand stock</option>
|
||||
<option value="made_to_order" {% if product.fulfillment == 'made_to_order' %}selected{% endif %}>Made to order</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>Stock qty</label><input name="stock_qty" type="number" value="{{ product.stock_qty|default:0 }}"></div>
|
||||
<div class="field"><label>Print minutes</label><input name="print_minutes" type="number" min="0" value="{{ product.print_minutes|default:0 }}"></div>
|
||||
<div class="field"><label>Filament grams</label><input name="filament_grams" type="number" min="0" value="{{ product.filament_grams|default:0 }}"></div>
|
||||
<div class="field"><label>Description</label><textarea name="description" style="min-height:120px">{{ product.description|default:'' }}</textarea></div>
|
||||
<label><input type="checkbox" name="is_published" {% if product.is_published %}checked{% endif %}> Published</label>
|
||||
<label><input type="checkbox" name="track_inventory" {% if product.track_inventory or not product %}checked{% endif %}> Track inventory</label>
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
</form>
|
||||
{% if product %}
|
||||
<form method="post" action="{% url 'shop_portal:product_stock' product.pk %}" class="form-grid" style="margin-top:24px">
|
||||
{% csrf_token %}
|
||||
<div class="field"><label>Adjust stock (+/−)</label><input name="delta" type="number" required value="1"></div>
|
||||
<button class="btn btn-ghost" type="submit">Apply adjustment</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends "portal_base.html" %}
|
||||
{% block title %}Products · Portal{% endblock %}
|
||||
{% block topbar_title %}Products{% endblock %}
|
||||
{% block portal_content %}
|
||||
<p><a class="btn btn-primary" href="{% url 'shop_portal:product_new' %}">New product</a></p>
|
||||
<table class="table">
|
||||
<thead><tr><th>Name</th><th>SKU</th><th>Price</th><th>Stock</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for product in products %}
|
||||
<tr>
|
||||
<td><a href="{% url 'shop_portal:product_edit' product.pk %}">{{ product.name }}</a></td>
|
||||
<td>{{ product.sku }}</td>
|
||||
<td>{{ product.price }} {{ product.currency|upper }}</td>
|
||||
<td>{{ product.stock_qty }}</td>
|
||||
<td>{% if product.is_published %}Published{% else %}Draft{% endif %}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="5" class="empty-state">No products yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Order {{ order.number }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section section-lg"><div class="container">
|
||||
<h1>Thank you</h1>
|
||||
<p>Order {{ order.number }} is {{ order.get_status_display|lower }}.</p>
|
||||
<p>A confirmation will go to {{ order.email }}.</p>
|
||||
</div></section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user