Initial commit - LEGO Instructions Manager v1.5.0

This commit is contained in:
2025-12-09 17:20:41 +11:00
commit 63496b1ccd
68 changed files with 9131 additions and 0 deletions

324
app/templates/sets/add.html Normal file
View File

@@ -0,0 +1,324 @@
{% extends "base.html" %}
{% block title %}Add Set - {{ app_name }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card shadow">
<div class="card-header bg-danger text-white">
<h3 class="mb-0">
<i class="bi bi-plus-circle"></i> Add New LEGO Set or MOC
</h3>
</div>
<div class="card-body">
<!-- Set Type Selection -->
<div class="mb-4 p-4 bg-light rounded border">
<h5 class="mb-3"><i class="bi bi-question-circle"></i> What are you adding?</h5>
<div class="row">
<div class="col-md-6 mb-3 mb-md-0">
<div class="form-check">
<input class="form-check-input" type="radio" name="set_type" id="type_official" value="official"
{% if request.args.get('type') != 'moc' %}checked{% endif %}>
<label class="form-check-label" for="type_official">
<strong><i class="bi bi-box-seam"></i> Official LEGO Set</strong>
<br>
<small class="text-muted">A set produced by LEGO with an official set number</small>
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="set_type" id="type_moc" value="moc"
{% if request.args.get('type') == 'moc' %}checked{% endif %}>
<label class="form-check-label" for="type_moc">
<strong><i class="bi bi-star-fill text-warning"></i> MOC (My Own Creation)</strong>
<br>
<small class="text-muted">A custom build designed by you or another builder</small>
</label>
</div>
</div>
</div>
</div>
{% if brickset_available %}
<!-- Brickset Search - Only for official sets -->
<div id="bricksetSection" class="mb-4 p-3 bg-light rounded">
<h5><i class="bi bi-search"></i> Search Brickset</h5>
<p class="text-muted small mb-3">Search for a set to auto-populate details</p>
<div class="input-group">
<input type="text" id="bricksetSearch" class="form-control"
placeholder="Enter set number or name...">
<button class="btn btn-primary" type="button" id="searchBtn">
<i class="bi bi-search"></i> Search
</button>
</div>
<div id="searchResults" class="mt-3"></div>
</div>
<hr id="bricksetDivider">
{% endif %}
<!-- Manual Entry Form -->
<form method="POST" action="{{ url_for('sets.add_set') }}" enctype="multipart/form-data">
<div class="row">
<div class="col-md-6 mb-3">
<label for="set_number" class="form-label">
Set Number <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="set_number"
name="set_number" required placeholder="e.g., 10497">
</div>
<div class="col-md-6 mb-3">
<label for="year_released" class="form-label">
Year Released <span class="text-danger">*</span>
</label>
<input type="number" class="form-control" id="year_released"
name="year_released" required min="1949" max="2030"
placeholder="e.g., 2024">
</div>
</div>
<div class="mb-3">
<label for="set_name" class="form-label">
Set Name <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="set_name"
name="set_name" required placeholder="e.g., Galaxy Explorer">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="theme" class="form-label">
Theme <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="theme"
name="theme" required placeholder="e.g., Space">
</div>
<div class="col-md-6 mb-3">
<label for="piece_count" class="form-label">
Piece Count
</label>
<input type="number" class="form-control" id="piece_count"
name="piece_count" placeholder="e.g., 1254">
</div>
</div>
<div class="mb-3">
<label for="image_url" class="form-label">
Image URL (optional)
</label>
<input type="url" class="form-control" id="image_url"
name="image_url" placeholder="https://...">
<div class="form-text">Enter a URL to an image of the set (e.g., from Brickset)</div>
</div>
<div class="mb-3">
<label for="cover_image" class="form-label">
<i class="bi bi-upload"></i> Upload Cover Picture
</label>
<input type="file" class="form-control" id="cover_image"
name="cover_image" accept="image/*">
<div class="form-text">
<i class="bi bi-info-circle"></i>
Upload your own photo of the set or MOC (JPG, PNG, GIF). Max 800px, optimized automatically.
</div>
<div id="imagePreview" class="mt-2" style="display: none;">
<img id="previewImg" src="" alt="Preview" style="max-width: 200px; max-height: 200px; border-radius: 8px;">
</div>
</div>
<!-- MOC (My Own Creation) Section -->
<div class="card mb-3 border-warning" id="mocSection" style="display: none;">
<div class="card-header bg-warning bg-opacity-25">
<h5 class="mb-0">
<i class="bi bi-star-fill text-warning"></i> MOC Information
</h5>
</div>
<div class="card-body">
<div class="alert alert-info mb-3">
<small>
<i class="bi bi-info-circle"></i>
<strong>MOC Tips:</strong>
<ul class="mb-0 mt-2">
<li>Use any set number format (e.g., MOC-001, CUSTOM-2024, MYBUILD-01)</li>
<li>Credit yourself or the original designer</li>
<li>Add notes about techniques, inspiration, or building tips</li>
</ul>
</small>
</div>
<input type="hidden" id="is_moc" name="is_moc" value="off">
<div class="mb-3">
<label for="moc_designer" class="form-label">
<i class="bi bi-person"></i> Designer / Creator Name <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="moc_designer"
name="moc_designer" placeholder="e.g., Your Name or Original Designer">
<div class="form-text">Who designed this MOC?</div>
</div>
<div class="mb-3">
<label for="moc_description" class="form-label">
<i class="bi bi-card-text"></i> Description / Build Notes
</label>
<textarea class="form-control" id="moc_description" name="moc_description"
rows="4" placeholder="Add details about your MOC, building techniques, inspiration, special features, etc."></textarea>
<div class="form-text">Share details about your custom creation</div>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between">
<a href="{{ url_for('sets.list_sets') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Cancel
</a>
<button type="submit" class="btn btn-danger btn-lg">
<i class="bi bi-plus-circle"></i> Add Set
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{% if brickset_available %}
<script>
$(document).ready(function() {
$('#searchBtn').click(function() {
const query = $('#bricksetSearch').val().trim();
if (!query) {
alert('Please enter a search term');
return;
}
$('#searchResults').html('<div class="text-center"><div class="spinner-border text-primary" role="status"></div></div>');
$.ajax({
url: '{{ url_for("sets.search_brickset") }}',
data: { q: query },
success: function(data) {
if (data.length === 0) {
$('#searchResults').html('<div class="alert alert-info">No results found</div>');
return;
}
let html = '<div class="list-group">';
data.forEach(function(set) {
html += `
<a href="#" class="list-group-item list-group-item-action search-result"
data-number="${set.setNumber}"
data-name="${set.name}"
data-theme="${set.theme}"
data-year="${set.year}"
data-pieces="${set.pieces || ''}"
data-image="${set.imageUrl || ''}">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">${set.setNumber}: ${set.name}</h6>
<small>${set.year}</small>
</div>
<small class="text-muted">${set.theme} - ${set.pieces || 'Unknown'} pieces</small>
</a>
`;
});
html += '</div>';
$('#searchResults').html(html);
// Handle click on search result
$('.search-result').click(function(e) {
e.preventDefault();
$('#set_number').val($(this).data('number'));
$('#set_name').val($(this).data('name'));
$('#theme').val($(this).data('theme'));
$('#year_released').val($(this).data('year'));
$('#piece_count').val($(this).data('pieces'));
$('#image_url').val($(this).data('image'));
$('#searchResults').html('<div class="alert alert-success">Form populated! Review and submit.</div>');
});
},
error: function() {
$('#searchResults').html('<div class="alert alert-danger">Search failed. Please try again.</div>');
}
});
});
// Allow enter key to search
$('#bricksetSearch').keypress(function(e) {
if (e.which === 13) {
e.preventDefault();
$('#searchBtn').click();
}
});
});
</script>
{% endif %}
<script>
$(document).ready(function() {
// Handle set type selection (Official vs MOC)
$('input[name="set_type"]').change(function() {
const isMoc = $('#type_moc').is(':checked');
if (isMoc) {
// Show MOC section, hide Brickset
$('#mocSection').slideDown();
$('#is_moc').val('on');
$('#bricksetSection').slideUp();
$('#bricksetDivider').hide();
// Clear Brickset populated fields (they might not apply to MOCs)
$('#image_url').val('');
// Update placeholder text for MOC context
$('#set_number').attr('placeholder', 'e.g., MOC-001, CUSTOM-2024');
$('#theme').attr('placeholder', 'e.g., Custom, Space MOCs, My Creations');
} else {
// Show Brickset, hide MOC section
$('#mocSection').slideUp();
$('#is_moc').val('off');
$('#bricksetSection').slideDown();
$('#bricksetDivider').show();
// Clear MOC fields
$('#moc_designer').val('');
$('#moc_description').val('');
// Reset placeholder text for official sets
$('#set_number').attr('placeholder', 'e.g., 10497');
$('#theme').attr('placeholder', 'e.g., Space');
}
});
// Initialize based on current selection (including URL parameter)
const selectedType = $('input[name="set_type"]:checked').val();
if (selectedType === 'moc') {
$('#type_moc').trigger('change');
} else {
$('#type_official').trigger('change');
}
// Image preview
$('#cover_image').change(function() {
const file = this.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
$('#previewImg').attr('src', e.target.result);
$('#imagePreview').slideDown();
};
reader.readAsDataURL(file);
} else {
$('#imagePreview').slideUp();
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,415 @@
{% extends "base.html" %}
{% block title %}{{ set.set_number }}: {{ set.set_name }} - {{ app_name }}{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('sets.list_sets') }}">Sets</a></li>
<li class="breadcrumb-item active">{{ set.set_number }}</li>
</ol>
</nav>
<div class="row">
<!-- Set Image and Details -->
<div class="col-lg-4 mb-4">
<div class="card shadow-sm">
{% if set.cover_image %}
<img src="{{ url_for('static', filename='uploads/' + set.cover_image.replace('\\', '/')) }}"
class="card-img-top" alt="{{ set.set_name }}"
style="max-height: 400px; object-fit: contain; background-color: #f8f9fa; padding: 20px;">
{% elif set.image_url %}
<img src="{{ set.image_url }}" class="card-img-top" alt="{{ set.set_name }}"
style="max-height: 400px; object-fit: contain; background-color: #f8f9fa; padding: 20px;">
{% else %}
<div class="d-flex align-items-center justify-content-center bg-light" style="height: 400px;">
<i class="bi bi-image display-1 text-muted"></i>
</div>
{% endif %}
<div class="card-body">
<h5 class="card-title">
{{ set.set_number }}
{% if set.is_moc %}
<span class="badge bg-warning text-dark">
<i class="bi bi-star-fill"></i> MOC
</span>
{% endif %}
</h5>
<h6 class="card-subtitle mb-3 text-muted">{{ set.set_name }}</h6>
<table class="table table-sm table-borderless">
<tr>
<th width="40%">Theme:</th>
<td><span class="badge bg-primary">{{ set.theme }}</span></td>
</tr>
<tr>
<th>Year:</th>
<td><span class="badge bg-warning text-dark">{{ set.year_released }}</span></td>
</tr>
{% if set.is_moc %}
<tr>
<th>Type:</th>
<td><span class="badge bg-info">My Own Creation</span></td>
</tr>
{% if set.moc_designer %}
<tr>
<th>Designer:</th>
<td>{{ set.moc_designer }}</td>
</tr>
{% endif %}
{% endif %}
{% if set.piece_count %}
<tr>
<th>Pieces:</th>
<td>{{ set.piece_count }}</td>
</tr>
{% endif %}
<tr>
<th>Instructions:</th>
<td>{{ set.instructions.count() }} file(s)</td>
</tr>
<tr>
<th>Added:</th>
<td>{{ set.created_at.strftime('%b %d, %Y') }}</td>
</tr>
</table>
</div>
<div class="card-footer bg-transparent">
<div class="d-grid gap-2">
<a href="{{ url_for('instructions.upload', set_id=set.id) }}" class="btn btn-success">
<i class="bi bi-cloud-upload"></i> Upload Instructions
</a>
<a href="{{ url_for('sets.edit_set', set_id=set.id) }}" class="btn btn-outline-secondary">
<i class="bi bi-pencil"></i> Edit Set Details
</a>
<form method="POST" action="{{ url_for('sets.delete_set', set_id=set.id) }}"
onsubmit="return confirm('Are you sure you want to delete this set and all its instructions?');">
<button type="submit" class="btn btn-outline-danger w-100">
<i class="bi bi-trash"></i> Delete Set
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Instructions -->
<div class="col-lg-8">
<div class="card shadow-sm mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-file-pdf"></i> Instructions</h5>
<a href="{{ url_for('instructions.upload', set_id=set.id) }}" class="btn btn-sm btn-success">
<i class="bi bi-plus-circle"></i> Upload
</a>
</div>
<div class="card-body">
{% if pdf_instructions or image_instructions %}
<!-- PDF Instructions (Books) -->
{% if pdf_instructions %}
<h6 class="mb-3"><i class="bi bi-book-fill text-danger"></i> PDF Instruction Books</h6>
<div class="row mb-4">
{% for instruction in pdf_instructions %}
<div class="col-md-6 col-lg-4 mb-3">
<div class="card h-100">
{% if instruction.thumbnail_path %}
<a href="{{ url_for('instructions.view', instruction_id=instruction.id) }}" target="_blank">
<img src="{{ url_for('static', filename='uploads/' + instruction.thumbnail_path.replace('\\', '/')) }}"
class="card-img-top"
alt="{{ instruction.file_name }}"
style="height: 200px; object-fit: contain; background-color: #f8f9fa; padding: 10px;">
</a>
{% else %}
<a href="{{ url_for('instructions.view', instruction_id=instruction.id) }}" target="_blank">
<div class="card-img-top d-flex align-items-center justify-content-center bg-light"
style="height: 200px;">
<i class="bi bi-file-pdf display-1 text-danger"></i>
</div>
</a>
{% endif %}
<div class="card-body">
<h6 class="card-title">{{ instruction.file_name }}</h6>
<p class="card-text small text-muted">
<i class="bi bi-file-pdf"></i> {{ instruction.file_size_mb }} MB<br>
<i class="bi bi-calendar"></i> {{ instruction.uploaded_at.strftime('%b %d, %Y') }}
</p>
</div>
<div class="card-footer bg-transparent">
<div class="d-flex gap-2">
<a href="{{ url_for('instructions.view', instruction_id=instruction.id) }}"
class="btn btn-sm btn-primary flex-fill" target="_blank">
<i class="bi bi-eye"></i> Open
</a>
<form method="POST" action="{{ url_for('instructions.delete', instruction_id=instruction.id) }}"
onsubmit="return confirm('Delete this PDF?');" class="flex-fill">
<button type="submit" class="btn btn-sm btn-outline-danger w-100">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Image Instructions (Single Card) -->
{% if image_instructions %}
<h6 class="mb-3"><i class="bi bi-images text-primary"></i> Scanned Instructions</h6>
<div class="row">
<div class="col-md-6 col-lg-4 mb-3">
<div class="card h-100">
{% set first_image = image_instructions[0] %}
<a href="{{ url_for('instructions.image_viewer', set_id=set.id) }}">
<img src="{{ url_for('static', filename='uploads/' + first_image.file_path.replace('\\', '/')) }}"
class="card-img-top"
alt="Instructions Preview"
style="height: 200px; object-fit: contain; background-color: #f8f9fa; padding: 10px; cursor: pointer;">
</a>
<div class="card-body">
<h6 class="card-title">
<i class="bi bi-file-image"></i> Image Instructions
</h6>
<p class="card-text small text-muted">
<i class="bi bi-files"></i> {{ image_instructions|length }} page(s)<br>
<i class="bi bi-calendar"></i> Uploaded {{ first_image.uploaded_at.strftime('%b %d, %Y') }}
</p>
</div>
<div class="card-footer bg-transparent">
<div class="d-flex gap-2">
<a href="{{ url_for('instructions.image_viewer', set_id=set.id) }}"
class="btn btn-sm btn-primary flex-fill">
<i class="bi bi-book-half"></i> View Instructions
</a>
<button type="button" class="btn btn-sm btn-outline-danger flex-fill"
data-bs-toggle="modal" data-bs-target="#deleteImagesModal">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Delete All Images Modal -->
<div class="modal fade" id="deleteImagesModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete All Image Instructions?</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>This will delete all {{ image_instructions|length }} image instruction pages.</p>
<p class="text-danger"><strong>This cannot be undone!</strong></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="POST" action="{{ url_for('instructions.delete_all_images', set_id=set.id) }}" style="display: inline;">
<button type="submit" class="btn btn-danger">Delete All Images</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="bi bi-file-earmark-x display-1 text-muted"></i>
<h5 class="mt-3">No Instructions Yet</h5>
<p class="text-muted">Upload PDF or image files to get started.</p>
<a href="{{ url_for('instructions.upload', set_id=set.id) }}" class="btn btn-success">
<i class="bi bi-cloud-upload"></i> Upload Instructions
</a>
</div>
{% endif %}
</div>
</div>
<!-- Extra Files Section -->
<div class="card shadow-sm mt-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-file-earmark-plus"></i> Extra Files</h5>
<a href="{{ url_for('extra_files.upload', set_id=set.id) }}" class="btn btn-sm btn-success">
<i class="bi bi-cloud-upload"></i> Upload Files
</a>
</div>
<div class="card-body">
{% set extra_files_list = set.extra_files.all() %}
{% if extra_files_list %}
<!-- Files Grid -->
<div class="row g-3">
{% for file in extra_files_list %}
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-light">
<!-- Preview for images -->
{% if file.is_image %}
<a href="{{ url_for('extra_files.preview', file_id=file.id) }}" target="_blank">
<img src="{{ url_for('extra_files.preview', file_id=file.id) }}"
class="card-img-top"
alt="{{ file.original_filename }}"
style="height: 150px; object-fit: cover; cursor: pointer;">
</a>
{% else %}
<div class="card-img-top bg-light d-flex align-items-center justify-content-center"
style="height: 150px;">
<i class="bi bi-{{ file.file_icon }} display-3 text-muted"></i>
</div>
{% endif %}
<div class="card-body p-2">
<h6 class="card-title small mb-1">
<i class="bi bi-{{ file.file_icon }}"></i>
{{ file.original_filename }}
</h6>
{% if file.category and file.category != 'other' %}
<span class="badge bg-info text-dark small mb-1">
{{ file.category|replace('_', ' ')|title }}
</span>
{% endif %}
<p class="card-text small text-muted mb-1">
<i class="bi bi-hdd"></i> {{ file.file_size_formatted }}
<br>
<i class="bi bi-calendar"></i> {{ file.uploaded_at.strftime('%b %d, %Y') }}
</p>
{% if file.description %}
<p class="card-text small text-muted mb-1">
{{ file.description|truncate(60) }}
</p>
{% endif %}
</div>
<div class="card-footer bg-transparent p-2">
<div class="d-flex gap-1">
{% if file.can_preview %}
<a href="{{ url_for('extra_files.preview', file_id=file.id) }}"
target="_blank"
class="btn btn-sm btn-outline-primary flex-fill"
title="Preview">
<i class="bi bi-eye"></i>
</a>
{% endif %}
<a href="{{ url_for('extra_files.download', file_id=file.id) }}"
class="btn btn-sm btn-outline-success flex-fill"
title="Download">
<i class="bi bi-download"></i>
</a>
<form method="POST"
action="{{ url_for('extra_files.delete', file_id=file.id) }}"
class="flex-fill"
onsubmit="return confirm('Delete {{ file.original_filename }}?');">
<button type="submit"
class="btn btn-sm btn-outline-danger w-100"
title="Delete">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-files display-3 text-muted"></i>
<p class="text-muted mt-2 mb-1">No extra files yet</p>
<p class="small text-muted">
Upload BrickLink XMLs, Stud.io files, box art, photos, or any other related files
</p>
<a href="{{ url_for('extra_files.upload', set_id=set.id) }}" class="btn btn-sm btn-success">
<i class="bi bi-cloud-upload"></i> Upload Files
</a>
</div>
{% endif %}
</div>
</div>
<!-- Set Information -->
<div class="card shadow-sm mt-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Additional Information</h5>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-4">Set Number:</dt>
<dd class="col-sm-8">{{ set.set_number }}</dd>
<dt class="col-sm-4">Set Name:</dt>
<dd class="col-sm-8">{{ set.set_name }}</dd>
<dt class="col-sm-4">Theme:</dt>
<dd class="col-sm-8">{{ set.theme }}</dd>
<dt class="col-sm-4">Year Released:</dt>
<dd class="col-sm-8">{{ set.year_released }}</dd>
{% if set.piece_count %}
<dt class="col-sm-4">Piece Count:</dt>
<dd class="col-sm-8">{{ set.piece_count }} pieces</dd>
{% endif %}
{% if set.is_moc %}
<dt class="col-sm-4">Type:</dt>
<dd class="col-sm-8">
<span class="badge bg-warning text-dark">
<i class="bi bi-star-fill"></i> My Own Creation (MOC)
</span>
</dd>
{% if set.moc_designer %}
<dt class="col-sm-4">Designer:</dt>
<dd class="col-sm-8">{{ set.moc_designer }}</dd>
{% endif %}
{% if set.moc_description %}
<dt class="col-sm-4">Description:</dt>
<dd class="col-sm-8">{{ set.moc_description }}</dd>
{% endif %}
{% endif %}
{% if set.brickset_id %}
<dt class="col-sm-4">Brickset ID:</dt>
<dd class="col-sm-8">{{ set.brickset_id }}</dd>
{% endif %}
<dt class="col-sm-4">Added By:</dt>
<dd class="col-sm-8">{{ set.added_by.username }}</dd>
<dt class="col-sm-4">Date Added:</dt>
<dd class="col-sm-8">{{ set.created_at.strftime('%B %d, %Y at %I:%M %p') }}</dd>
<dt class="col-sm-4">Last Updated:</dt>
<dd class="col-sm-8">{{ set.updated_at.strftime('%B %d, %Y at %I:%M %p') }}</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="mt-4">
<a href="{{ url_for('sets.list_sets') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Back to Sets
</a>
</div>
{% endblock %}
{% block extra_css %}
<style>
.instruction-thumbnail {
cursor: pointer;
transition: transform 0.2s;
}
.instruction-thumbnail:hover {
transform: scale(1.05);
}
</style>
{% endblock %}

View File

@@ -0,0 +1,211 @@
{% extends "base.html" %}
{% block title %}Edit Set - {{ app_name }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">
<i class="bi bi-pencil"></i> Edit LEGO Set
</h3>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('sets.edit_set', set_id=set.id) }}" enctype="multipart/form-data">
<div class="row">
<div class="col-md-6 mb-3">
<label for="set_number" class="form-label">
Set Number <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="set_number"
name="set_number" value="{{ set.set_number }}" disabled>
<div class="form-text">Set number cannot be changed</div>
</div>
<div class="col-md-6 mb-3">
<label for="year_released" class="form-label">
Year Released <span class="text-danger">*</span>
</label>
<input type="number" class="form-control" id="year_released"
name="year_released" value="{{ set.year_released }}"
required min="1949" max="2030">
</div>
</div>
<div class="mb-3">
<label for="set_name" class="form-label">
Set Name <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="set_name"
name="set_name" value="{{ set.set_name }}" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="theme" class="form-label">
Theme <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="theme"
name="theme" value="{{ set.theme }}" required>
</div>
<div class="col-md-6 mb-3">
<label for="piece_count" class="form-label">
Piece Count
</label>
<input type="number" class="form-control" id="piece_count"
name="piece_count" value="{{ set.piece_count or '' }}">
</div>
</div>
<div class="mb-3">
<label for="image_url" class="form-label">
Image URL (optional)
</label>
<input type="url" class="form-control" id="image_url"
name="image_url" value="{{ set.image_url or '' }}">
<div class="form-text">Enter a URL to an image of the set</div>
</div>
{% if set.image_url %}
<div class="mb-3">
<label class="form-label">Current URL Image Preview:</label>
<div class="border rounded p-3 bg-light">
<img src="{{ set.image_url }}" alt="{{ set.set_name }}"
style="max-height: 200px; max-width: 100%; object-fit: contain;">
</div>
</div>
{% endif %}
<!-- Cover Image Upload -->
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-image"></i> Cover Picture</h6>
</div>
<div class="card-body">
{% if set.cover_image %}
<div class="mb-3">
<label class="form-label">Current Uploaded Cover:</label>
<div class="border rounded p-3 bg-light position-relative">
<img src="{{ url_for('static', filename='uploads/' + set.cover_image.replace('\\', '/')) }}"
alt="{{ set.set_name }}"
style="max-height: 200px; max-width: 100%; object-fit: contain;">
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="remove_cover_image" name="remove_cover_image">
<label class="form-check-label text-danger" for="remove_cover_image">
<i class="bi bi-trash"></i> Remove uploaded cover image
</label>
</div>
</div>
{% endif %}
<div class="mb-0">
<label for="cover_image" class="form-label">
<i class="bi bi-upload"></i> {% if set.cover_image %}Replace Cover Picture{% else %}Upload Cover Picture{% endif %}
</label>
<input type="file" class="form-control" id="cover_image"
name="cover_image" accept="image/*">
<div class="form-text">
Upload your own photo of the set or MOC (JPG, PNG, GIF). Max 800px, optimized automatically.
</div>
<div id="imagePreview" class="mt-2" style="display: none;">
<label class="form-label small">New Image Preview:</label>
<img id="previewImg" src="" alt="Preview" style="max-width: 200px; max-height: 200px; border-radius: 8px; border: 2px solid #dee2e6;">
</div>
</div>
</div>
</div>
<!-- MOC (My Own Creation) Section -->
<div class="card mb-3 border-info">
<div class="card-header bg-info bg-opacity-10">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_moc" name="is_moc"
{% if set.is_moc %}checked{% endif %}>
<label class="form-check-label fw-bold" for="is_moc">
<i class="bi bi-star-fill text-warning"></i> This is a MOC (My Own Creation)
</label>
</div>
</div>
<div class="card-body" id="mocFields" {% if not set.is_moc %}style="display: none;"{% endif %}>
<div class="mb-3">
<label for="moc_designer" class="form-label">
Designer / Creator Name
</label>
<input type="text" class="form-control" id="moc_designer"
name="moc_designer" value="{{ set.moc_designer or '' }}"
placeholder="e.g., Your Name">
<div class="form-text">Who designed this MOC?</div>
</div>
<div class="mb-3">
<label for="moc_description" class="form-label">
Description / Notes
</label>
<textarea class="form-control" id="moc_description" name="moc_description"
rows="4" placeholder="Add details about your MOC...">{{ set.moc_description or '' }}</textarea>
<div class="form-text">Optional notes about your custom creation</div>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between">
<a href="{{ url_for('sets.view_set', set_id=set.id) }}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Cancel
</a>
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-check-circle"></i> Save Changes
</button>
</div>
</form>
</div>
</div>
<div class="card shadow mt-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Set Information</h5>
</div>
<div class="card-body">
<p><strong>Added by:</strong> {{ set.added_by.username }}</p>
<p><strong>Created:</strong> {{ set.created_at.strftime('%B %d, %Y at %I:%M %p') }}</p>
<p><strong>Last updated:</strong> {{ set.updated_at.strftime('%B %d, %Y at %I:%M %p') }}</p>
<p class="mb-0"><strong>Instructions:</strong> {{ set.instructions.count() }} file(s)</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
// Toggle MOC fields visibility
$('#is_moc').change(function() {
if ($(this).is(':checked')) {
$('#mocFields').slideDown();
} else {
$('#mocFields').slideUp();
}
});
// Image preview
$('#cover_image').change(function() {
const file = this.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
$('#previewImg').attr('src', e.target.result);
$('#imagePreview').slideDown();
};
reader.readAsDataURL(file);
} else {
$('#imagePreview').slideUp();
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,192 @@
{% extends "base.html" %}
{% block title %}My Sets - {{ app_name }}{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-md-8">
<h1><i class="bi bi-grid"></i> My LEGO Sets</h1>
<p class="text-muted">{{ pagination.total }} sets in your collection</p>
</div>
<div class="col-md-4 text-end">
<a href="{{ url_for('sets.add_set') }}" class="btn btn-danger">
<i class="bi bi-plus-circle"></i> Add New Set
</a>
</div>
</div>
<!-- Search and Filters -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" action="{{ url_for('sets.list_sets') }}" class="row g-3">
<div class="col-md-4">
<label for="search" class="form-label">Search</label>
<input type="text" class="form-control" id="search" name="q"
value="{{ search_query }}" placeholder="Set number or name...">
</div>
<div class="col-md-3">
<label for="theme" class="form-label">Theme</label>
<select class="form-select" id="theme" name="theme">
<option value="">All Themes</option>
{% for theme in themes %}
<option value="{{ theme }}" {% if theme == current_theme %}selected{% endif %}>
{{ theme }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="year" class="form-label">Year</label>
<select class="form-select" id="year" name="year">
<option value="">All Years</option>
{% for year in years %}
<option value="{{ year }}" {% if year == current_year %}selected{% endif %}>
{{ year }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="sort" class="form-label">Sort By</label>
<select class="form-select" id="sort" name="sort">
<option value="set_number" {% if current_sort == 'set_number' %}selected{% endif %}>Set Number</option>
<option value="name" {% if current_sort == 'name' %}selected{% endif %}>Name</option>
<option value="theme" {% if current_sort == 'theme' %}selected{% endif %}>Theme</option>
<option value="year" {% if current_sort == 'year' %}selected{% endif %}>Year</option>
<option value="newest" {% if current_sort == 'newest' %}selected{% endif %}>Newest First</option>
</select>
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-search"></i>
</button>
</div>
</form>
</div>
</div>
<!-- Sets Grid -->
{% if sets %}
<div class="row">
{% for set in sets %}
<div class="col-md-6 col-lg-4 col-xl-3 mb-4">
<div class="card h-100 shadow-sm">
<a href="{{ url_for('sets.view_set', set_id=set.id) }}">
{% if set.cover_image %}
<img src="{{ url_for('static', filename='uploads/' + set.cover_image.replace('\\', '/')) }}"
class="card-img-top set-image" alt="{{ set.set_name }}">
{% elif set.image_url %}
<img src="{{ set.image_url }}" class="card-img-top set-image" alt="{{ set.set_name }}">
{% else %}
<div class="card-img-top set-image d-flex align-items-center justify-content-center bg-light">
<i class="bi bi-image display-1 text-muted"></i>
</div>
{% endif %}
</a>
<div class="card-body">
<h6 class="card-title">
<a href="{{ url_for('sets.view_set', set_id=set.id) }}" class="text-decoration-none">
{{ set.set_number }}
</a>
{% if set.is_moc %}
<span class="badge bg-warning text-dark" title="My Own Creation">
<i class="bi bi-star-fill"></i>
</span>
{% endif %}
</h6>
<p class="card-text small text-truncate" title="{{ set.set_name }}">
{{ set.set_name }}
</p>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="badge bg-primary">{{ set.theme }}</span>
<span class="badge bg-warning text-dark">{{ set.year_released }}</span>
</div>
{% if set.piece_count %}
<p class="card-text small text-muted mb-2">
<i class="bi bi-grid-3x3"></i> {{ set.piece_count }} pieces
</p>
{% endif %}
<p class="card-text small">
<i class="bi bi-file-pdf"></i> {{ set.instructions.count() }} instruction(s)
</p>
</div>
<div class="card-footer bg-transparent">
<div class="btn-group w-100" role="group">
<a href="{{ url_for('sets.view_set', set_id=set.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> View
</a>
<a href="{{ url_for('sets.edit_set', set_id=set.id) }}"
class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i> Edit
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if pagination.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<li class="page-item {% if not pagination.has_prev %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('sets.list_sets', page=pagination.prev_num, sort=current_sort, theme=current_theme, year=current_year, q=search_query) }}">
Previous
</a>
</li>
{% for page_num in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if page_num %}
<li class="page-item {% if page_num == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('sets.list_sets', page=page_num, sort=current_sort, theme=current_theme, year=current_year, q=search_query) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
<li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('sets.list_sets', page=pagination.next_num, sort=current_sort, theme=current_theme, year=current_year, q=search_query) }}">
Next
</a>
</li>
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="bi bi-inbox display-1 text-muted"></i>
<h3 class="mt-3">No Sets Found</h3>
<p class="text-muted">
{% if search_query or current_theme or current_year %}
Try adjusting your filters or search terms.
{% else %}
Start by adding your first LEGO set or MOC to your collection!
{% endif %}
</p>
<div class="d-flex justify-content-center gap-2">
<a href="{{ url_for('sets.add_set') }}" class="btn btn-danger">
<i class="bi bi-box-seam"></i> Add Official Set
</a>
<a href="{{ url_for('sets.add_set') }}?type=moc" class="btn btn-warning">
<i class="bi bi-star-fill"></i> Add MOC
</a>
</div>
</a>
</div>
{% endif %}
{% endblock %}