Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

10 changed files with 59 additions and 419 deletions

View File

@ -4,8 +4,6 @@ A web application for organizing and tracking LEGO sets, parts, and minifigures.
> **Screenshots at the end of the readme!**
<a href="https://www.buymeacoffee.com/frederikb" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="41" width="174"></a>
## Features
- Track multiple LEGO sets with their parts and minifigures
@ -35,15 +33,9 @@ mkdir static/{sets,instructions,parts,minifigs}
```
REBRICKABLE_API_KEY=your_api_key_here
DOMAIN_NAME=https://your.domain.com
LINKS=True
RANDOM=True
```
- If using locally, set `DOMAIN_NAME` to `http://localhost:3333`.
- `LINKS`: Set to `True` in order for set numbers to be links to Rebrickable on the front page.
- `RANDOM`: Set to `True` in order for the sets to shuffle on the frontpage each load.
If using locally, set `DOMAIN_NAME` to `http://localhost:3333`.
3. Deploy with Docker Compose:
```bash
@ -64,7 +56,15 @@ mkdir -p static/{sets,instructions,parts,minifigs}
touch app.db
```
2. Create Docker Compose file:
2. Create a `.env` file with your configuration:
```
REBRICKABLE_API_KEY=your_api_key_here
DOMAIN_NAME=https://your.domain.com
```
If using locally, set `DOMAIN_NAME` to `http://localhost:3333`.
3. Create Docker Compose file:
```bash
services:
bricktracker:
@ -74,21 +74,14 @@ services:
ports:
- "3333:3333"
volumes:
- ./.env:/app/.env
- ./static/parts:/app/static/parts
- ./static/instructions:/app/static/instructions
- ./static/sets:/app/static/sets
- ./static/minifigs:/app/static/minifigs
- ./app.db:/app/app.db
environment:
- REBRICKABLE_API_KEY=your_api_key_here
- DOMAIN_NAME=https://your.domain.com
- LINKS=True #optional, enables set numbers to be Rebrickable links on the front page
- RANDOM=False #optional, set to True if you want your front page to be shuffled on load
```
If using locally, set `DOMAIN_NAME` to `http://localhost:3333`.
3. Deploy with Docker Compose:
4. Deploy with Docker Compose:
```bash
docker compose up -d
```
@ -123,8 +116,6 @@ Instructions can be added to the `static/instructions` folder. Instructions **mu
Instructions are not automatically downloaded!
Instructions can be uploaded from the webinterface (desktop-only) using the `Upload` button in the navbar.
## Docker Configuration
The application uses two main configuration files:
@ -220,12 +211,6 @@ Sets are added from rebrickable using your own API key. Set numbers are checked
### Wishlist
![](https://xbackbone.baerentsen.space/LaMU8/qUYeHAKU93.png/raw)
![](https://xbackbone.baerentsen.space/LaMU8/hACAbArO44.png/raw)
Sets are added from rebrickable and again checked against sets.csv. If you can't add a brand new set, consider updating your data from the [`/config` page](https://xbackbone.baerentsen.space/LaMU8/lErImaCE12.png/raw). Press the delete button to remove a set. Known Issue: If multiple sets of the same number is added, they will all be deleted.
Wishlist uses *unofficial* retirement data.
Each set number, links to bricklink for pricecheck.

168
app.py
View File

@ -12,14 +12,9 @@ import rebrick #rebrickable api
import requests # request img from web
import shutil # save img locally
import eventlet
from collections import defaultdict
import plotly.express as px
import pandas as pd
from downloadRB import download_and_unzip,get_nil_images,get_retired_sets
from db import initialize_database,get_rows,delete_tables
from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.utils import secure_filename
app = Flask(__name__)
@ -27,21 +22,8 @@ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_
socketio = SocketIO(app,cors_allowed_origins=os.getenv("DOMAIN_NAME"))
count = 0
if os.getenv("RANDOM") == 'True':
RANDOM = True
else:
RANDOM = False
if os.getenv("LINKS"):
LINKS = os.getenv("LINKS")
else:
LINKS = False
DIRECTORY = os.path.join(os.getcwd(), 'static', 'instructions')
UPLOAD_FOLDER = DIRECTORY
ALLOWED_EXTENSIONS = {'pdf'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
@app.route('/favicon.ico')
@ -93,49 +75,6 @@ def hyphen_split(a):
return a.split("-")[0]
return "-".join(a.split("-", 2)[:2])
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload',methods=['GET','POST'])
def uploadInst():
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
# If the user does not select a file, the browser submits an
# empty file without a filename.
if file.filename == '':
flash('No selected file')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return redirect('/')
return '''
<!doctype html>
<title>Upload instructions</title>
<h1>Upload instructions</h1>
<p>Files must be named like:</p>
<code>&lt;set number&gt;-&lt;version&gt;-&lt;part&gt;.pdf</code>
<ul>
<li><code>7595-1.pdf</code> for set 7595</li>
<li><code>71039-2.pdf</code> for Moon Knight in <code>Collectible Minifigures: Marvel Series 2</code></li>
<li><code>71039-13.pdf</code> for the whole set <code>Collectible Minifigures: Marvel Series 2</code></li>
<li><code>10294-1-1.pdf</code> for the 1st pdf in the 10294 set
<li><code>10294-1-2.pdf</code> for the 2nd pdf in the 10294 set
<li><code>10294-1-3.pdf</code> for the 3rd pdf in the 10294 set
<li><code>10937-1-0.pdf</code> for the comic that comes with set 10937.
<li><code>10937-1-1.pdf</code> for the 1st pdf in the 10937 set
</ul>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>
'''
@app.route('/delete/<tmp>',methods=['POST', 'GET'])
def delete(tmp):
@ -497,9 +436,7 @@ def config():
def missing():
conn = sqlite3.connect('app.db')
cursor = conn.cursor()
#cursor.execute("SELECT part_num, color_id, element_id, part_img_url_id, SUM(quantity) AS total_quantity, GROUP_CONCAT(set_num, ', ') AS set_number FROM missing GROUP BY part_num, color_id, element_id;")
cursor.execute("SELECT part_num, color_id, element_id, part_img_url_id, SUM(quantity) AS total_quantity, GROUP_CONCAT(set_num || ',' || u_id, ',') AS set_number FROM missing GROUP BY part_num, color_id, element_id, part_img_url_id ORDER BY part_num;")
cursor.execute('SELECT part_num, color_id, element_id, part_img_url_id, SUM(quantity) AS total_quantity FROM missing GROUP BY part_num, color_id, element_id;')
results = cursor.fetchall()
missing_list = [list(i) for i in results]
@ -521,7 +458,7 @@ def missing():
def parts():
conn = sqlite3.connect('app.db')
cursor = conn.cursor()
cursor.execute('SELECT id, part_num, color_id, color_name, element_id, part_img_url_id, SUM(quantity) AS total_quantity, name FROM inventory GROUP BY part_num, part_img_url_id, color_id, color_name, element_id, name;')
cursor.execute('SELECT id, part_num, color_id, color_name, element_id, part_img_url_id, SUM(quantity) AS total_quantity, name FROM inventory GROUP BY id, part_num, part_img_url_id, color_id, color_name, element_id, name;')
results = cursor.fetchall()
missing_list = [list(i) for i in results]
@ -674,8 +611,7 @@ def index():
results = cursor.fetchall()
set_list = [list(i) for i in results]
if RANDOM:
random.shuffle(set_list)
cursor.execute('SELECT DISTINCT u_id from missing;')
results = cursor.fetchall()
missing_list = [list(i)[0] for i in results]
@ -697,11 +633,11 @@ def index():
files = [f for f in os.listdir(DIRECTORY) if f.endswith('.pdf')]
#files = [re.match(r'^([\w]+-[\w]+)', f).group() for f in os.listdir(DIRECTORY) if f.endswith('.pdf')]
files.sort()
print(files.sort())
return render_template('index.html',set_list=set_list,themes_list=theme_file,missing_list=missing_list,files=files,minifigs=minifigs,links=LINKS)
return render_template('index.html',set_list=set_list,themes_list=theme_file,missing_list=missing_list,files=files,minifigs=minifigs)
if request.method == 'POST':
if request.method == 'post':
set_num = request.form.get('set_num')
u_id = request.form.get('u_id')
minif = request.form.get('minif')
@ -1089,97 +1025,5 @@ def save_number(tmp):
return Response(status=204)
@app.route('/dashboard')
def dashboard():
# Connect to the SQLite database
conn = sqlite3.connect('app.db')
cursor = conn.cursor()
# Execute the query
cursor.execute("SELECT year, set_num, theme_id FROM sets")
rows = cursor.fetchall()
# Initialize defaultdict to count occurrences
theme_counts = defaultdict(int)
year_counts = defaultdict(int)
# Count unique occurrences (removing duplicates)
seen = set() # To track unique combinations
for year, set_num, theme_id in rows:
# Create a unique identifier for each entry
entry_id = f"{year}-{set_num}-{theme_id}"
if entry_id not in seen:
theme_counts[theme_id] += 1
year_counts[year] += 1
seen.add(entry_id)
# Convert to regular dictionaries and sort
sets_by_theme = dict(sorted(
{k: v for k, v in theme_counts.items() if v > 1}.items()
))
sets_by_year = dict(sorted(
{k: v for k, v in year_counts.items() if v > 0}.items()
))
# Graphs using Plotly
fig_sets_by_theme = px.bar(
x=list(sets_by_theme.keys()),
y=list(sets_by_theme.values()),
labels={'x': 'Theme ID', 'y': 'Number of Sets'},
title='Number of Sets by Theme'
)
fig_sets_by_year = px.line(
x=list(sets_by_year.keys()),
y=list(sets_by_year.values()),
labels={'x': 'Year', 'y': 'Number of Sets'},
title='Number of Sets Released Per Year'
)
most_frequent_parts = {
"Brick 1 x 1": 866,
"Plate 1 x 1": 782,
"Plate 1 x 2": 633,
"Plate Round 1 x 1 with Solid Stud": 409,
"Tile 1 x 2 with Groove": 382,
}
minifigs_by_set = {"10217-1": 12, "7595-1": 8, "10297-1": 7, "21338-1": 4, "4865-1": 4}
missing_parts_by_set = {"10297-1": 4, "10280-1": 1, "21301-1": 1, "21338-1": 1, "7595-1": 1}
fig_parts = px.bar(
x=list(most_frequent_parts.keys()),
y=list(most_frequent_parts.values()),
labels={'x': 'Part Name', 'y': 'Quantity'},
title='Most Frequent Parts'
)
fig_minifigs = px.bar(
x=list(minifigs_by_set.keys()),
y=list(minifigs_by_set.values()),
labels={'x': 'Set Number', 'y': 'Number of Minifigures'},
title='Minifigures by Set'
)
fig_missing_parts = px.bar(
x=list(missing_parts_by_set.keys()),
y=list(missing_parts_by_set.values()),
labels={'x': 'Set Number', 'y': 'Missing Parts Count'},
title='Missing Parts by Set'
)
# Convert graphs to HTML
graphs = {
"sets_by_theme": fig_sets_by_theme.to_html(full_html=False),
"sets_by_year": fig_sets_by_year.to_html(full_html=False),
"parts": fig_parts.to_html(full_html=False),
"minifigs": fig_minifigs.to_html(full_html=False),
"missing_parts": fig_missing_parts.to_html(full_html=False),
}
return render_template("dashboard.html", graphs=graphs)
if __name__ == '__main__':
socketio.run(app.run(host='0.0.0.0', debug=True, port=3333))

View File

@ -1,8 +1,6 @@
flask
flask_socketio
pathlib
plotly
pandas
numpy
rebrick
requests

View File

@ -14,12 +14,6 @@
<style>
.title-image {
object-fit: cover;
width: 100%;
height: 200px;
}
table.sortable th:not(.sorttable_sorted):not(.sorttable_sorted_reverse):not(.sorttable_nosort):after {
content: " \25B4\25BE"
}
@ -34,7 +28,6 @@ table.sortable tbody tr:nth-child(2n+1) td {
display: none;
}
table {
width: 100%; /* Ensure the table takes full width of its container */
@ -253,8 +246,8 @@ background-color: white;
<div class="container">
<center>
<h1 class="title is-2 mt-4">{{ tmp }} - {{ title }}</h1>
<img class="lightbox-trigger title-image mb-5" id="cover" style='height: 150px; width: auto; object-fit: contain' src="/static/sets/{{ tmp }}.jpg" alt="{{ tmp }} - {{ title }}">
<h1 class="title">{{ tmp }} - {{ title }}</h1>
<img class="lightbox-trigger" id="cover" style='height: 150px; width: auto; object-fit: contain' src="/static/sets/{{ tmp }}.jpg" alt="{{ tmp }} - {{ title }}">
{% block content %}{% endblock %}
</div>
{% block scripts %}{% endblock %}

View File

@ -1,60 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LEGO Dashboard</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f4f4f4;
}
h1 {
text-align: center;
color: #333;
}
.chart-container {
margin-bottom: 50px;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.chart-container h2 {
text-align: center;
color: #555;
}
</style>
</head>
<body>
<h1>LEGO Dashboard</h1>
<div class="chart-container">
<h2>Sets by Theme</h2>
{{ graphs['sets_by_theme']|safe }}
</div>
<div class="chart-container">
<h2>Sets Released Per Year</h2>
{{ graphs['sets_by_year']|safe }}
</div>
<div class="chart-container">
<h2>Most Frequent Parts</h2>
{{ graphs['parts']|safe }}
</div>
<div class="chart-container">
<h2>Minifigures by Set</h2>
{{ graphs['minifigs']|safe }}
</div>
<div class="chart-container">
<h2>Missing Parts by Set</h2>
{{ graphs['missing_parts']|safe }}
</div>
</body>
</html>

View File

@ -12,11 +12,6 @@
<style>
.my-input::placeholder {
color: var(--bulma-grey-dark);
}
@media only screen and (max-width: 480px) {
/* horizontal scrollbar for tables if mobile screen */
.hidden-mobile {
@ -134,10 +129,6 @@ display: none !important;
<a class="navbar-item hidden-mobile" href="/config">
Config
</a>
<span></span>
<a class="navbar-item hidden-mobile" href="/upload">
Upload
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navMenu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
@ -168,9 +159,6 @@ display: none !important;
<a class="navbar-item" id="toggleButton4">
Sets with missing pieces
</a>
<a class="navbar-item" id="toggleButton5">
Missing instructions
</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
@ -208,8 +196,10 @@ display: none !important;
</div>
</div>
</nav>
<div class="search-container">
<input class="input my-input" type="text" id="searchInput" onkeyup="searchFunction()" placeholder="Search title, set number, theme or parts...">
<input class="input"type="text" id="searchInput" onkeyup="searchFunction()" placeholder="Search title or set number...">
<!-- <center hidden="true">
<button class="button button-outline" onclick="dynamicSort('set_id')">Sort by ID</button>
<button class="button button-outline" onclick="dynamicSort('set_year')">Sort by Year</button>
@ -228,20 +218,17 @@ display: none !important;
<!-- Add more buttons for other text values if needed -->
</div>
<div class="grid-container" id="gridContainer">
{% for i in set_list %}
<div class="grid-item">
<div class="card">
<div class="columns" style="">
<div class="column is-full" style="text-align: left;">
<p class="is-size-5 searchTitle">
{% if links == 'True' %}
<a style="font-weight: bold;" href="https://rebrickable.com/sets/{{ i[0] }}" target="_blank" class="has-text-dark set_id">{{ i[0] }}</a> <span style="font-weight: bold;" class="has-text-dark set_name">{{ i[1] }}</span><br>
{% else %}
<span style="font-weight: bold;" class="has-text-dark set_id">{{ i[0] }}</span> <span style="font-weight: bold;" class="has-text-dark set_name">{{ i[1] }}</span><br>
{% endif %}
<span style="font-weight: bold;" class="set_id">{{ i[0] }}</span> <span style="font-weight: bold;" class="set_name">{{ i[1] }}</span><br>
<a class="is-size-7 set_theme" style="color: #363636;">{{ i[3] }}</a> <a class="is-size-7" style="color: #363636;"> (<span class='set_year'>{{ i[2] }}</span>)</a>
<span></span>
<a class="is-size-5" style="color: #363636;float:right;"><b>Parts:</b> <span class='set_parts'>{{ i[4] }}</span></a>
<a class="is-size-5" style="color: #363636;float:right;"><b>Parts:</b> {{ i[4] }}</a>
</p>
</div>
<!--<div class="column" style="text-align: left;">
@ -323,6 +310,7 @@ display: none !important;
</p>
{% set ns = namespace(found=false) %}
{% for file in files %}
{% if ns.found is sameas false and file.startswith(i[0]) %}
@ -330,7 +318,7 @@ display: none !important;
<div class="dropdown-trigger">
<button class="is-size-6" aria-haspopup="true" aria-controls="dropdown-menu3">
<span>
<a id="inst-link" class="is-size-6" style="color: #363636;">Inst.</a>
<a class="is-size-6" style="color: #363636;">Inst.</a>
</span>
</button>
</div>
@ -339,7 +327,7 @@ display: none !important;
<!-- <a class="js-modal-trigger is-size-6" style="color: #363636;" data-id="{{ i[0] }}" data-target="modal-inst">Inst.</a> -->
{% for x in files %}
{% if x.startswith(i[0]) %}
<a href="/files/{{ x }}" target="_blank" class="dropdown-item">{{ x }}</a>
<a href="/files/{{ x }}" class="dropdown-item">{{ x }}</a>
{% endif %}
{% endfor %}
@ -354,6 +342,9 @@ display: none !important;
<a class="is-size-6" style="color: #363636;" href="/{{ i[0] }}/{{ i[11] }}">Inv.</a>
</span>
</p>
</footer>
</div>
</div>
@ -721,36 +712,6 @@ display: none !important;
}
});
document.addEventListener('DOMContentLoaded', function () {
const toggleButton = document.getElementById('toggleButton5');
let isHidden = true; // Initially, only show checked grid items
// Initialize visibility based on isHidden
updateVisibility();
toggleButton.addEventListener('click', function() {
// Toggle visibility and update grid items
isHidden = !isHidden;
updateVisibility();
});
function updateVisibility() {
// Get all grid items
const gridItems = document.querySelectorAll('.grid-item');
// Iterate over each grid item
gridItems.forEach(function(item) {
const hasInst = item.querySelector('#inst-link');
if (isHidden || !hasInst ) {
// Show the grid item if it's hidden or the checkbox is checked
item.style.display = 'block';
} else {
// Hide the grid item if the checkbox is not checked
item.style.display = 'none';
}
});
}
});
function customSort(a, b) {
// Function to remove leading articles

View File

@ -25,17 +25,6 @@ table.sortable tbody tr:nth-child(2n+1) td {
background: #ecf0f1;
}
table {
width: 100%; /* Ensure the table takes full width of its container */
}
td {
overflow: hidden;
text-overflow: ellipsis;
word-wrap: break-word;
}
@media only screen and (max-width: 480px) {
/* horizontal scrollbar for tables if mobile screen */
@ -84,9 +73,6 @@ td {
text-overflow: ellipsis;
}
.fixed-width {
max-width:100px;
}
/* Modal Styles */
@ -178,46 +164,36 @@ td {
</div>
</div>
</nav>
<div class="container">
<center>
<h1 class="title is-2 mt-4">Missing Pieces</h1>
<div style="overflow-x:auto;border-radius: 10px;border: 1px #ccc solid; box-shadow:0 0.5em 1em -0.125em hsla(221deg,14%,4%,0.1),0 0px 0 1px hsla(221deg,14%,4%,0.02);" >
<table id="data" class="table sortable tablemobile">
<div class="center-table" >
<table id="data" class="table sortable tablemobile">
<thead>
<tr>
<th style="width:65px;" class="fixed-width sorttable_nosort"></th>
<th class="fixed-width" >Part Num</th>
<th class="fixed-width" >Color</th>
<th class="fixed-width" >Element ID</th>
<th class="fixed-width" >Qty</th>
<th class="fixed-width sorttable_nosort">Sets</th>
<th class="sorttable_nosort"></th>
<th >Part Num</th>
<th >Color</th>
<th >Element ID</th>
<th >Qty</th>
</tr>
</thead>
<tbody>
{% for brick in missing_list %}
<tr>
{% if brick[4] == 'nil' %}
<td style="background-color: #ffffff;"><img src="{{ '/static/none.jpg' }}" class="lightbox-trigger" alt="{{ brick[3] }}" style="height: 50px; width: 50px;margin:0;padding: 0;" loading="lazy"></td>
<td><img src="{{ '/static/none.jpg' }}" class="lightbox-trigger" alt="{{ brick[3] }}" style="height: 50px; width: 50px;margin:0;padding: 0;" loading="lazy"></td>
{% else %}
<td style="background-color: #ffffff;"><img src="{{ '/static/parts/' + brick[3] + '.jpg' }}" alt="{{ brick[3] }}" class="lightbox-trigger" style="height: 50px; width: 50px;margin:0;padding: 0;" loading="lazy"></td>
<td><img src="{{ '/static/parts/' + brick[3] + '.jpg' }}" alt="{{ brick[3] }}" class="lightbox-trigger" style="height: 50px; width: 50px;margin:0;padding: 0;" loading="lazy"></td>
{% endif %}
<td><a target="_blank" href="https://www.bricklink.com/v2/catalog/catalogitem.page?P={{ brick[0] }}">{{ brick[0] }}</a></td>
<td>{{ brick[1] }}</td>
<td><a target="_blank" href="https://www.rebrickable.com/elements/{{ brick[2] }}">{{ brick[2] }}</a></td>
<td>{{ brick[4] }}</td>
<td>
{% set set_numbers = brick[5].split(',') %}
{% for i in range(0, set_numbers|length, 2) %}
<a href="{{ set_numbers[i] }}/{{ set_numbers[i+1] }}">{{ set_numbers[i] }}</a>{% if i != set_numbers|length - 2 %},{% endif %}
{% endfor %}
</td>
<td>{{ brick[4] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</center>
</div>
<div id="lightbox-modal">

View File

@ -24,23 +24,6 @@ table.sortable tbody tr:nth-child(2n+1) td {
background: #ecf0f1;
}
.my-input::placeholder {
color: var(--bulma-grey-dark);
}
.name-class {
max-width: 300px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.search-container {
padding: 10px;
}
@media only screen and (max-width: 480px) {
/* horizontal scrollbar for tables if mobile screen */
img {
@ -183,21 +166,15 @@ table.sortable tbody tr:nth-child(2n+1) td {
</div>
</div>
</nav>
<div class="container">
<center>
<h1 class="title is-2 mt-4">Parts</h1>
<div class="search-container">
<input class="input my-input" type="text" id="searchInput" onkeyup="searchFunction()" placeholder="Search number, color, name or element id...">
</div>
<div class="center-table" >
<div style="overflow-x:auto;border-radius: 10px;border: 1px #ccc solid; box-shadow:0 0.5em 1em -0.125em hsla(221deg,14%,4%,0.1),0 0px 0 1px hsla(221deg,14%,4%,0.02);" >
<table id="data" class="table tablemobile sortable" style="widht:100%;height:100%;table-layout: fixed;">
<table id="data" class="table tablemobile sortable">
<thead>
<tr>
<th style="width:65px;" class="sorttable_nosort"></th>
<th class="sorttable_nosort"></th>
<th >Part Num</th>
<th >Color</th>
<th class="name-class">Name</th>
<th class="name-class" >Name</th>
<th >element_id</th>
<th >total_quantity</th>
</tr>
@ -219,10 +196,8 @@ table.sortable tbody tr:nth-child(2n+1) td {
{% endfor %}
</tbody>
</table>
</div>
</div>
</center>
</div>
<div id="lightbox-modal">
<div class="lightbox-wrapper">
<span class="close">&times;</span>
@ -274,37 +249,6 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
function searchFunction() {
// Get input element and filter value
var input = document.getElementById('searchInput');
var filter = input.value.toUpperCase();
// Get the table and its rows
var table = document.getElementById('data');
var rows = table.getElementsByTagName('tr');
// Loop through all rows (skip the header row)
for (var i = 1; i < rows.length; i++) {
var cells = rows[i].getElementsByTagName('td');
var rowMatches = false;
// Loop through each cell in the row
for (var j = 0; j < cells.length; j++) {
var cellContent = cells[j].textContent || cells[j].innerText;
if (cellContent.toUpperCase().indexOf(filter) > -1) {
rowMatches = true;
break;
}
}
// Show or hide the row based on the match
rows[i].style.display = rowMatches ? '' : 'none';
}
}
</script>
</body>
</html>

View File

@ -1,15 +1,15 @@
{% extends "base.html" %}
{% block content %}
<div style="overflow-x:auto;border-radius: 10px;border: 1px #ccc solid; box-shadow:0 0.5em 1em -0.125em hsla(221deg,14%,4%,0.1),0 0px 0 1px hsla(221deg,14%,4%,0.02);" >
<div style="overflow-x:auto;">
<table id="data" class="table tablemobile sortable">
<thead>
<tr>
<th class="sorttable_nosort" style="width:65px;">Image</th>
<th class="hidden-mobile name-class">Name</th>
<th class="hidden-mobile">Color</th>
<th style="text-align:center">Qty</th>
<th class="sorttable_nosort" style="text-align:right;">Missing</th>
<th >Qty</th>
<th class="sorttable_nosort" style="text-align:center;">Missing</th>
</tr>
</thead>
<tbody>
@ -58,12 +58,11 @@
</table>
</div>
<div id="minifigs" style="margin: 2rem 0; padding: 2rem; border-bottom: 2px solid #eee;"></div>
{% if minifig_list | length > 0 %}
<h1 class="title is-2 ">Minifigs</h1>
<h1 id="minifigs" class="title">Minifigs</h1>
{% for fig in minifig_list %}
<h2 class="subtitle is-4 mt-4 ">{{ fig[2] }} ({{ fig[0] }})</h2>
<h2 class="subtitle">{{ fig[2] }} ({{ fig[0] }})</h2>
<div style="display: flex; justify-content: center;">
<div style="display: flex; align-items: center;">
@ -76,15 +75,15 @@
</div>
</div>
<div class="center-table" style="overflow-x:auto;border-radius: 10px;border: 1px #ccc solid; box-shadow:0 0.5em 1em -0.125em hsla(221deg,14%,4%,0.1),0 0px 0 1px hsla(221deg,14%,4%,0.02);" >
<table id="data" class="table tablemobile sortable">
<div class="center-table" >
<table id="data" class="table">
<thead>
<tr>
<th class="fixed-width sorttable_nosort"></th>
<th class="fixed-width"></th>
<th style="text-align:left;margin:0px;" class="fixed-width hidden-mobile name-class">Name</th>
<th class="fixed-width hidden-mobile">Color</th>
<th class="fixed-width" style="text-align: center;">Qty</th>
<th class="fixed-width sorttable_nosort" style="text-align: right;">Missing</th>
<th class="fixed-width" style="text-align: center;">Missing</th>
</tr>
</thead>
<tbody>
@ -102,7 +101,7 @@
<td style="text-align:left;margin:0px;" class="hidden-mobile name-class">{{ part[3] }}</td>
<td class="hidden-mobile">{{ part[7] }}</td>
<td style="text-align: center;">{{ part[8] * fig[3] }}</td>
<td class="centered-cell" style="text-align:right;">
<td class="centered-cell">
<div class="inputContainer">
{% set ns = namespace(count='') %}
<form id="number-form">
@ -142,7 +141,7 @@
<div id="lightbox-modal">
<div class="lightbox-wrapper">
<span class="close">&times;</span>
<img style="background-color: white;" class="lightbox-content" id="lightbox-image">
<img class="lightbox-content" id="lightbox-image">
<div class="text-container" id="lightbox-text"></div>
</div>
</div>

View File

@ -242,7 +242,7 @@ background-color: white;
{% for sets in wishlist %}
<tr>
<td><img src="{{ '/static/sets/' + sets[0] + '.jpg' }}" class="lightbox-trigger" style="height: 50px; width: auto;"></td>
<td style="text-align:center;margin:0px;" ><a class="has-text-dark" href="https://www.bricklink.com/v2/catalog/catalogitem.page?S={{ sets[0] }}" target="_blank">{{ sets[0] }}</a></td>
<td style="text-align:center;margin:0px;">{{ sets[0] }}</td>
<td>{{ sets[1] }}</td>
<td style="text-align:center;" class="hidden-mobile">{{ sets[2] }}</td>
<td style="text-align:center;" class="hidden-mobile">{{ sets[4] }}</td>