Initial commit - LEGO Instructions Manager v1.5.0
This commit is contained in:
324
app/templates/sets/add.html
Normal file
324
app/templates/sets/add.html
Normal 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 %}
|
||||
415
app/templates/sets/detail.html
Normal file
415
app/templates/sets/detail.html
Normal 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 %}
|
||||
211
app/templates/sets/edit.html
Normal file
211
app/templates/sets/edit.html
Normal 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 %}
|
||||
192
app/templates/sets/list.html
Normal file
192
app/templates/sets/list.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user