first commit
This commit is contained in:
commit
32964d1d1f
227
app.py
Normal file
227
app.py
Normal file
@ -0,0 +1,227 @@
|
||||
from flask import Flask, render_template, request, redirect, url_for, jsonify
|
||||
import json
|
||||
import requests
|
||||
import logging
|
||||
import os
|
||||
import traceback
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Setup logging to file
|
||||
logging.basicConfig(filename='log.log', level=logging.DEBUG, format='%(asctime)s %(levelname)s:%(message)s')
|
||||
|
||||
# Load the assort data
|
||||
ASSORT_FILE_PATH = 'assort.json'
|
||||
QUEST_ASSORT_FILE_PATH = 'questassort.json'
|
||||
CACHE_FILE_PATH = 'item_cache.json'
|
||||
RUBLE_TPL_ID = '5449016a4bdc2d6f028b456f'
|
||||
|
||||
try:
|
||||
with open(ASSORT_FILE_PATH) as f:
|
||||
assort_data = json.load(f)
|
||||
logging.debug("Assort data loaded successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Error loading assort data: {e}")
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
try:
|
||||
with open(QUEST_ASSORT_FILE_PATH) as f:
|
||||
quest_assort_data = json.load(f)
|
||||
logging.debug("Quest assort data loaded successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Error loading quest assort data: {e}")
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
# Load cache or initialize an empty dictionary
|
||||
if os.path.exists(CACHE_FILE_PATH):
|
||||
try:
|
||||
with open(CACHE_FILE_PATH) as f:
|
||||
item_cache = json.load(f)
|
||||
logging.debug("Item cache loaded successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Error loading item cache: {e}")
|
||||
logging.error(traceback.format_exc())
|
||||
item_cache = {}
|
||||
else:
|
||||
item_cache = {}
|
||||
logging.debug("Initialized empty item cache")
|
||||
|
||||
# Tarkov.dev API URL
|
||||
TARKOV_API_URL = "https://api.tarkov.dev/graphql"
|
||||
|
||||
def get_ruble_image():
|
||||
return get_item_details_cached(RUBLE_TPL_ID)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
try:
|
||||
items = get_main_items_with_details(assort_data)
|
||||
ruble_image = get_ruble_image()
|
||||
return render_template('index.html', items=items, ruble_image=ruble_image)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in index route: {e}")
|
||||
logging.error(traceback.format_exc())
|
||||
return "Error loading items", 500
|
||||
|
||||
def get_main_items_with_details(assort_data):
|
||||
items = []
|
||||
for item in assort_data['items']:
|
||||
if item['parentId'] == 'hideout':
|
||||
item_copy = item.copy() # Create a copy of the item to avoid modifying the original
|
||||
item_copy['details'] = get_item_details_cached(item_copy['_tpl'])
|
||||
item_copy['parts'] = get_item_parts_with_details(item_copy['_id'], assort_data)
|
||||
item_copy['barter_scheme'] = get_barter_scheme_with_details(item_copy['_id'], assort_data)
|
||||
item_copy['quest_requirement'] = get_quest_requirement(item_copy['_id'])
|
||||
item_copy['offer_name'] = item_copy['_id'] # Add offer name
|
||||
items.append(item_copy)
|
||||
logging.debug(f"Main items with details: {items}")
|
||||
return items
|
||||
|
||||
def get_item_details_cached(tpl):
|
||||
if tpl in item_cache:
|
||||
logging.debug(f"Cache hit for tpl {tpl}")
|
||||
return item_cache[tpl]
|
||||
else:
|
||||
logging.debug(f"Cache miss for tpl {tpl}, fetching from API")
|
||||
item_details = get_items_details([tpl])
|
||||
if tpl in item_details:
|
||||
item_cache[tpl] = item_details[tpl]
|
||||
save_item_cache()
|
||||
return item_details.get(tpl, {})
|
||||
|
||||
def get_items_details(tpls):
|
||||
queries = "\n".join([
|
||||
f'item{index}: item(id: "{tpl}") {{ id name shortName description basePrice image512pxLink wikiLink }}'
|
||||
for index, tpl in enumerate(tpls)
|
||||
])
|
||||
query = f"{{ {queries} }}"
|
||||
try:
|
||||
response = requests.post(TARKOV_API_URL, json={'query': query})
|
||||
data = response.json()
|
||||
logging.debug(f"Item details for tpls {tpls}: {data}")
|
||||
return {tpl: data['data'][f'item{index}'] for index, tpl in enumerate(tpls)}
|
||||
except Exception as e:
|
||||
logging.error(f"Error fetching item details for tpls {tpls}: {e}")
|
||||
logging.error(traceback.format_exc())
|
||||
return {}
|
||||
|
||||
def get_item_parts_with_details(parent_id, assort_data):
|
||||
parts = []
|
||||
def fetch_parts(parent_id):
|
||||
sub_parts = []
|
||||
for item in assort_data['items']:
|
||||
if item['parentId'] == parent_id:
|
||||
part_copy = item.copy() # Create a copy of the part to avoid modifying the original
|
||||
part_copy['details'] = get_item_details_cached(part_copy['_tpl'])
|
||||
part_copy['parts'] = fetch_parts(part_copy['_id']) # Fetch sub-parts recursively
|
||||
sub_parts.append(part_copy)
|
||||
return sub_parts
|
||||
parts = fetch_parts(parent_id)
|
||||
logging.debug(f"Parts for parent_id {parent_id}: {parts}")
|
||||
return parts
|
||||
|
||||
def get_barter_scheme_with_details(item_id, assort_data):
|
||||
barter_scheme = []
|
||||
if 'barter_scheme' in assort_data:
|
||||
for scheme in assort_data['barter_scheme'].get(item_id, []):
|
||||
scheme_details = []
|
||||
for req in scheme:
|
||||
req_copy = req.copy() # Create a copy of the req to avoid modifying the original
|
||||
req_details = get_item_details_cached(req_copy['_tpl'])
|
||||
req_copy['details'] = req_details
|
||||
scheme_details.append(req_copy)
|
||||
barter_scheme.append(scheme_details)
|
||||
logging.debug(f"Barter scheme for item_id {item_id}: {barter_scheme}")
|
||||
return barter_scheme
|
||||
|
||||
def get_quest_requirement(item_id):
|
||||
for quest_type, quests in quest_assort_data.items():
|
||||
if item_id in quests:
|
||||
return {
|
||||
"type": quest_type,
|
||||
"quest": quests[item_id]
|
||||
}
|
||||
return None
|
||||
|
||||
@app.route('/edit/<item_id>', methods=['GET', 'POST'])
|
||||
def edit_item(item_id):
|
||||
try:
|
||||
if request.method == 'POST':
|
||||
# Handle form submission
|
||||
new_barter_scheme = []
|
||||
tpl_list = request.form.getlist('barter_tpl')
|
||||
count_list = request.form.getlist('barter_count')
|
||||
scheme = []
|
||||
for i in range(len(tpl_list)):
|
||||
scheme.append({
|
||||
'_tpl': tpl_list[i],
|
||||
'count': int(count_list[i])
|
||||
})
|
||||
new_barter_scheme.append(scheme)
|
||||
update_barter_scheme(item_id, new_barter_scheme)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
item = next((item for item in assort_data['items'] if item['_id'] == item_id), None)
|
||||
if item:
|
||||
item_copy = item.copy() # Create a copy of the item to avoid modifying the original
|
||||
item_copy['details'] = get_item_details_cached(item_copy['_tpl'])
|
||||
item_copy['parts'] = get_item_parts_with_details(item_id, assort_data)
|
||||
item_copy['barter_scheme'] = get_barter_scheme_with_details(item_id, assort_data)
|
||||
item_copy['quest_requirement'] = get_quest_requirement(item_id)
|
||||
ruble_image = get_ruble_image()
|
||||
return render_template('edit.html', item=item_copy, ruble_image=ruble_image)
|
||||
else:
|
||||
return "Item not found", 404
|
||||
except Exception as e:
|
||||
logging.error(f"Error in edit_item route: {e}")
|
||||
logging.error(traceback.format_exc())
|
||||
return "Error editing item", 500
|
||||
|
||||
def update_barter_scheme(item_id, new_barter_scheme):
|
||||
try:
|
||||
assort_data['barter_scheme'][item_id] = new_barter_scheme
|
||||
save_assort_data()
|
||||
except Exception as e:
|
||||
logging.error(f"Error updating barter scheme for item {item_id}: {e}")
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
def save_assort_data():
|
||||
try:
|
||||
# Deep copy the assort data and remove 'details' key before saving
|
||||
cleaned_assort_data = json.loads(json.dumps(assort_data)) # Deep copy
|
||||
|
||||
# Clean items
|
||||
for item in cleaned_assort_data['items']:
|
||||
if 'details' in item:
|
||||
del item['details']
|
||||
|
||||
# Clean barter_scheme
|
||||
for item_id, schemes in cleaned_assort_data.get('barter_scheme', {}).items():
|
||||
for scheme in schemes:
|
||||
for part in scheme:
|
||||
if 'details' in part:
|
||||
del part['details']
|
||||
|
||||
with open(ASSORT_FILE_PATH, 'w') as f:
|
||||
json.dump(cleaned_assort_data, f, indent=2) # Use 2 spaces for indentation
|
||||
logging.debug("Assort data saved successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Error saving assort data: {e}")
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
def save_item_cache():
|
||||
try:
|
||||
with open(CACHE_FILE_PATH, 'w') as f:
|
||||
json.dump(item_cache, f, indent=2) # Use 2 spaces for indentation
|
||||
logging.debug("Item cache saved successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Error saving item cache: {e}")
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
@app.route('/item_image/<tpl>')
|
||||
def item_image(tpl):
|
||||
details = get_item_details_cached(tpl)
|
||||
return jsonify({'image512pxLink': details.get('image512pxLink', ''), 'name': details.get('name', '')})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Flask>=2.0.3
|
||||
requests>=2.26.0
|
179
templates/edit.html
Normal file
179
templates/edit.html
Normal file
@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Edit Item</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
|
||||
<style>
|
||||
.price-box {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.price-box img {
|
||||
width: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.price-box span {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.price-box small {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
color: #888;
|
||||
}
|
||||
.card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.card-header img {
|
||||
width: 45px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.parts-tree {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.parts-tree > li {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.part-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: #f8f9fa;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.part-item img {
|
||||
width: 30px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Edit {{ item.details.name }}</h1>
|
||||
<div class="form-group">
|
||||
<label for="main-item">Main Item</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">
|
||||
<img src="{{ item.details.image512pxLink }}" alt="{{ item.details.name }}" style="width: 50px;">
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="main-item" name="main-item" value="{{ item.details.name }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-box">
|
||||
<img src="{{ ruble_image.image512pxLink }}" alt="RUB">
|
||||
<div>
|
||||
<span>{{ item.details.basePrice }} RUB</span>
|
||||
<small>Base Price (for reference)</small>
|
||||
</div>
|
||||
</div>
|
||||
{% if item.parts|length > 0 %}
|
||||
<h2>
|
||||
<button class="btn btn-outline-primary-custom" type="button" data-toggle="collapse" data-target="#parts-{{ item._id }}" aria-expanded="false" aria-controls="parts-{{ item._id }}">
|
||||
Parts
|
||||
</button>
|
||||
</h2>
|
||||
<div id="parts-{{ item._id }}" class="collapse">
|
||||
<ul class="parts-tree">
|
||||
{% include 'parts.html' with context %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h2>Barter Scheme</h2>
|
||||
<form method="POST" id="barter-form">
|
||||
<div id="barter-scheme">
|
||||
{% for scheme in item.barter_scheme %}
|
||||
<div class="form-group barter-item" data-index="{{ loop.index0 }}">
|
||||
{% for req in scheme %}
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">
|
||||
<img src="{{ req.details.image512pxLink }}" alt="{{ req.details.name }}" style="width: 30px;" id="barter_img_{{ loop.index0 }}">
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" class="form-control barter_tpl" id="barter_tpl_{{ loop.index0 }}" name="barter_tpl" value="{{ req._tpl }}" required>
|
||||
<input type="number" class="form-control barter_count" id="barter_count_{{ loop.index0 }}" name="barter_count" value="{{ req.count }}" required>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-danger remove-barter-item" type="button">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="add-barter-item">Add Barter Item</button>
|
||||
<button type="submit" class="btn btn-success">Save</button>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
{% if item.quest_requirement %}
|
||||
<h2>Quest Requirement</h2>
|
||||
<p>
|
||||
<strong>Quest Requirement:</strong>
|
||||
{{ item.quest_requirement.quest }}
|
||||
{% if item.quest_requirement.type == 'success' %}
|
||||
<i class="fas fa-check-circle text-success"></i>
|
||||
{% elif item.quest_requirement.type == 'fail' %}
|
||||
<i class="fas fa-times-circle text-danger"></i>
|
||||
{% elif item.quest_requirement.type == 'started' %}
|
||||
<i class="fas fa-question-circle text-primary"></i>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
let barterIndex = {{ item.barter_scheme|length }};
|
||||
|
||||
$('#add-barter-item').click(function() {
|
||||
const newItem = `
|
||||
<div class="form-group barter-item" data-index="${barterIndex}">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">
|
||||
<img src="" alt="Barter Item" style="width: 30px;" id="barter_img_${barterIndex}">
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" class="form-control barter_tpl" id="barter_tpl_${barterIndex}" name="barter_tpl" required>
|
||||
<input type="number" class="form-control barter_count" id="barter_count_${barterIndex}" name="barter_count" required>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-danger remove-barter-item" type="button">Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
$('#barter-scheme').append(newItem);
|
||||
barterIndex++;
|
||||
});
|
||||
|
||||
$(document).on('click', '.remove-barter-item', function() {
|
||||
if ($('.barter-item').length > 1) {
|
||||
$(this).closest('.barter-item').remove();
|
||||
} else {
|
||||
alert("You must have at least one barter item.");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('change', '.barter_tpl', function() {
|
||||
const index = $(this).closest('.barter-item').data('index');
|
||||
const tpl = $(this).val();
|
||||
$.get(`/item_image/${tpl}`, function(data) {
|
||||
$(`#barter_img_${index}`).attr('src', data.image512pxLink);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
142
templates/index.html
Normal file
142
templates/index.html
Normal file
@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Trader Assort</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
|
||||
<style>
|
||||
.price-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
background-color: #f8f9fa;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.price-box img {
|
||||
width: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.price-box span {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.price-box small {
|
||||
font-size: 0.8em;
|
||||
color: #888;
|
||||
}
|
||||
.card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.card-header img {
|
||||
width: 45px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.barter-list {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.barter-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.barter-list li img {
|
||||
width: 45px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.parts-tree {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.parts-tree > li {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.part-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: #f8f9fa;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.part-item img {
|
||||
width: 30px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Trader Assort</h1>
|
||||
<div class="list-group">
|
||||
{% for item in items %}
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center">
|
||||
<img src="{{ item.details.image512pxLink }}" alt="{{ item.details.name }}">
|
||||
<strong>{{ item.details.name }}</strong>
|
||||
<div class="ml-2 text-muted">{{ item.offer_name }}</div> <!-- Add offer name here -->
|
||||
<div class="price-box ml-auto">
|
||||
<div><img src="{{ ruble_image.image512pxLink }}" alt="RUB"><span>{{ item.details.basePrice }} RUB</span></div>
|
||||
<small>Base Price (for reference)</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a href="{{ url_for('edit_item', item_id=item._id) }}" class="btn btn-primary btn-sm float-right">Edit</a>
|
||||
|
||||
{% if item.parts|length > 0 %}
|
||||
<h2>
|
||||
<button class="btn btn-outline-primary" type="button" data-toggle="collapse" data-target="#parts-{{ item._id }}" aria-expanded="false" aria-controls="parts-{{ item._id }}">
|
||||
Parts
|
||||
</button>
|
||||
</h2>
|
||||
<div id="parts-{{ item._id }}" class="collapse">
|
||||
<ul class="parts-tree">
|
||||
{% include 'parts.html' with context %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if item.barter_scheme|length > 0 %}
|
||||
<div><strong>Barter Requirements:</strong></div>
|
||||
<ul class="barter-list">
|
||||
{% for scheme in item.barter_scheme %}
|
||||
{% for req in scheme %}
|
||||
<li>
|
||||
<span>{{ req.count }} ×</span>
|
||||
<img src="{{ req.details.image512pxLink }}" alt="{{ req.details.name }}">
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if item.quest_requirement %}
|
||||
<p>
|
||||
<strong>Quest Requirement:</strong>
|
||||
{{ item.quest_requirement.quest }}
|
||||
{% if item.quest_requirement.type == 'success' %}
|
||||
<i class="fas fa-check-circle text-success"></i>
|
||||
{% elif item.quest_requirement.type == 'fail' %}
|
||||
<i class="fas fa-times-circle text-danger"></i>
|
||||
{% elif item.quest_requirement.type == 'started' %}
|
||||
<i class="fas fa-question-circle text-primary"></i>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
16
templates/parts.html
Normal file
16
templates/parts.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% macro render_parts(parts) %}
|
||||
{% for part in parts %}
|
||||
<li class="part-item">
|
||||
<img src="{{ part.details.image512pxLink }}" alt="{{ part.details.name }}">
|
||||
<div>
|
||||
<strong>{{ part.details.shortName }}</strong>
|
||||
{% if part.parts|length > 0 %}
|
||||
<ul class="parts-tree">
|
||||
{{ render_parts(part.parts) }}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
{{ render_parts(item.parts) }}
|
Loading…
x
Reference in New Issue
Block a user