Compare commits
No commits in common. "master" and "master" have entirely different histories.
41
README.md
41
README.md
@ -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
168
app.py
@ -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><set number>-<version>-<part>.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))
|
||||
|
@ -1,8 +1,6 @@
|
||||
flask
|
||||
flask_socketio
|
||||
pathlib
|
||||
plotly
|
||||
pandas
|
||||
numpy
|
||||
rebrick
|
||||
requests
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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">×</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>
|
||||
|
@ -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">×</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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user