fixed implementation of UGC content uploader

This commit is contained in:
2026-08-03 18:57:39 -05:00
parent 39e31d3980
commit 473b3c811b
40 changed files with 4137 additions and 87 deletions
+3
View File
@@ -38,6 +38,9 @@
<a href="{% url 'landing:home' %}">
<i class="fas fa-home"></i><span class="nav-text"> Home</span>
</a>
<a href="{% url 'library:browse' %}">
<i class="fas fa-compass"></i><span class="nav-text"> Browse</span>
</a>
</nav>
</div>
<main class="main-content">
+18 -65
View File
@@ -10,8 +10,8 @@
<p>Hosted datapacks for Needs Of Nature &amp; Animation Director.</p>
<p class="home-hero-sub">Browse and download packs shared by the community. Upload your own once you have a creator account.</p>
<div class="home-hero-actions">
<a href="#" class="btn btn-primary"><i class="fas fa-download"></i> Browse packs</a>
<a href="#" class="btn btn-secondary"><i class="fas fa-upload"></i> Upload a pack</a>
<a href="{% url 'library:browse' %}" class="btn btn-primary"><i class="fas fa-download"></i> Browse packs</a>
<a href="{% url 'library:project_create' %}" class="btn btn-secondary"><i class="fas fa-upload"></i> Upload a pack</a>
</div>
</div>
</div>
@@ -19,22 +19,22 @@
<div class="home-stats">
<div class="stat-card">
<i class="fas fa-users"></i>
<span class="stat-value">0</span>
<span class="stat-value">{{ stats.creators }}</span>
<span class="stat-label">Creators</span>
</div>
<div class="stat-card">
<i class="fas fa-box-open"></i>
<span class="stat-value">0</span>
<span class="stat-value">{{ stats.packs }}</span>
<span class="stat-label">Packs</span>
</div>
<div class="stat-card">
<i class="fas fa-download"></i>
<span class="stat-value">0</span>
<span class="stat-value">{{ stats.downloads }}</span>
<span class="stat-label">Downloads</span>
</div>
<div class="stat-card">
<i class="fas fa-tag"></i>
<span class="stat-value">0</span>
<span class="stat-value">{{ stats.versions }}</span>
<span class="stat-label">Versions</span>
</div>
</div>
@@ -42,70 +42,23 @@
<div class="home-section">
<div class="home-section-header">
<h2>Latest packs</h2>
<a href="#" class="home-section-link">View all <i class="fas fa-arrow-right"></i></a>
<a href="{% url 'library:browse' %}" class="home-section-link">View all <i class="fas fa-arrow-right"></i></a>
</div>
<div class="pack-grid">
{% comment %} Example cards — replace with real Pack objects when the library model lands. {% endcomment %}
<a href="#" class="pack-card">
<div class="pack-card-thumb">
<img src="{% static 'Logo.png' %}" alt="">
</div>
<div class="pack-card-body">
<h3>Example Pack</h3>
<p class="pack-card-author"><i class="fas fa-user"></i> a_creator</p>
<p class="pack-card-desc">Placeholder description for a datapack.</p>
<div class="pack-card-meta">
<span class="pack-card-version"><i class="fas fa-tag"></i> v1.0.0</span>
<span class="pack-card-downloads"><i class="fas fa-download"></i> 0</span>
{% for project in latest_projects %}
{% include 'library/_project_card.html' %}
{% empty %}
<div class="pack-card pack-card-empty">
<div class="pack-card-thumb">
<i class="fas fa-box-open"></i>
</div>
<div class="pack-card-body">
<h3>No packs yet</h3>
<p class="pack-card-desc">Be the first to share a pack.</p>
</div>
</div>
</a>
<a href="#" class="pack-card">
<div class="pack-card-thumb">
<img src="{% static 'Logo.png' %}" alt="">
</div>
<div class="pack-card-body">
<h3>Another Pack</h3>
<p class="pack-card-author"><i class="fas fa-user"></i> someone</p>
<p class="pack-card-desc">This card shows what the library grid will look like.</p>
<div class="pack-card-meta">
<span class="pack-card-version"><i class="fas fa-tag"></i> v0.9.2</span>
<span class="pack-card-downloads"><i class="fas fa-download"></i> 0</span>
</div>
</div>
</a>
<a href="#" class="pack-card">
<div class="pack-card-thumb">
<img src="{% static 'Logo.png' %}" alt="">
</div>
<div class="pack-card-body">
<h3>Third Example</h3>
<p class="pack-card-author"><i class="fas fa-user"></i> dev</p>
<p class="pack-card-desc">Static assets, profile pictures and zip downloads land under /media/&lt;user&gt;/&lt;pack&gt;/.</p>
<div class="pack-card-meta">
<span class="pack-card-version"><i class="fas fa-tag"></i> v1.1.0</span>
<span class="pack-card-downloads"><i class="fas fa-download"></i> 0</span>
</div>
</div>
</a>
<a href="#" class="pack-card">
<div class="pack-card-thumb">
<img src="{% static 'Logo.png' %}" alt="">
</div>
<div class="pack-card-body">
<h3>And Another</h3>
<p class="pack-card-author"><i class="fas fa-user"></i> tester</p>
<p class="pack-card-desc">Responsive grid, Catppuccin-styled, ready for real data.</p>
<div class="pack-card-meta">
<span class="pack-card-version"><i class="fas fa-tag"></i> v0.1.0</span>
<span class="pack-card-downloads"><i class="fas fa-download"></i> 0</span>
</div>
</div>
</a>
{% endfor %}
</div>
</div>
@@ -0,0 +1,24 @@
{% load static %}
<a href="{% url 'library:project_detail' project.slug %}" class="pack-card">
<div class="pack-card-thumb">
{% if project.thumbnail %}
<img src="{{ project.thumbnail_url }}" alt="">
{% else %}
<i class="fas fa-box-open pack-card-thumb-icon"></i>
{% endif %}
</div>
<div class="pack-card-body">
<h3>{{ project.title }}</h3>
<p class="pack-card-author"><i class="fas fa-user"></i> {{ project.owner.username }}</p>
<p class="pack-card-desc">{{ project.summary|default:"No description yet." }}</p>
<div class="pack-card-tags">
{% for link in project.tag_links.all|slice:":4" %}
<span class="tag-chip" style="border-color: {{ link.tag.category.color }}; color: {{ link.tag.category.color }};">{{ link.tag.name }}</span>
{% endfor %}
</div>
<div class="pack-card-meta">
<span class="pack-card-version"><i class="fas fa-tag"></i> {{ project.versions.0.version_name|default:"no versions" }}</span>
<span class="pack-card-downloads"><i class="fas fa-download"></i> {{ project.downloads }}</span>
</div>
</div>
</a>
@@ -0,0 +1,26 @@
<div class="form-group">
{{ form.title.errors }}
<label for="{{ form.title.id_for_label }}">Title</label>
{{ form.title }}
</div>
<div class="form-group">
{{ form.summary.errors }}
<label for="{{ form.summary.id_for_label }}">Summary</label>
{{ form.summary }}
</div>
<div class="form-row">
<div class="form-group">
{{ form.category.errors }}
<label for="{{ form.category.id_for_label }}">Category</label>
{{ form.category }}
</div>
</div>
<div class="form-group">
{{ form.description.errors }}
<label for="{{ form.description.id_for_label }}">Description</label>
{{ form.description }}
<p class="bio-help"><i class="fas fa-info-circle"></i> Supports Markdown — headings, lists, links, code, tables and more.</p>
</div>
@@ -0,0 +1,17 @@
<div class="form-group">
{{ form.thumbnail.errors }}
<label for="{{ form.thumbnail.id_for_label }}">Thumbnail</label>
{{ form.thumbnail }}
<p class="bio-help"><i class="fas fa-info-circle"></i> Square image shown on cards and the project header.</p>
</div>
<div class="form-group">
<label>Tags</label>
<div class="tag-editor">
<input type="text" id="tag-input" placeholder="Type a tag and press Enter… (e.g. content:threesome, species:dragon)" autocomplete="off">
<div class="tag-suggestions" id="tag-suggestions" hidden></div>
<div class="tag-chips" id="tag-chips"></div>
{{ form.tags }}
</div>
<p class="bio-help"><i class="fas fa-info-circle"></i> Categories: loader, version, general, creator, content, species, non_version. Unprefixed tags go to General. Creator tags are added automatically.</p>
</div>
@@ -0,0 +1,18 @@
<div class="form-group">
{{ form.version_name.errors }}
<label for="{{ form.version_name.id_for_label }}">{{ form.version_name.label }}</label>
{{ form.version_name }}
</div>
<div class="form-group">
{{ form.file.errors }}
<label for="{{ form.file.id_for_label }}">{{ form.file.label }}</label>
{{ form.file }}
</div>
<div class="form-group">
{{ form.changelog.errors }}
<label for="{{ form.changelog.id_for_label }}">{{ form.changelog.label }}</label>
{{ form.changelog }}
<p class="bio-help"><i class="fas fa-info-circle"></i> Supports Markdown.</p>
</div>
@@ -0,0 +1,45 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Add Media - {{ project.title }}{% endblock %}
{% block content %}
<h1><i class="fas fa-images"></i> Add media — {{ project.title }}</h1>
<p><a href="{% url 'library:project_detail' project.slug %}">&larr; Back to project</a></p>
<form method="post" class="card project-form" id="asset-form">
{% csrf_token %}
<div class="upload-zone" data-upload="media">
<div class="drop-zone" role="button" tabindex="0">
<div class="drop-zone-inner">
<i class="fas fa-cloud-upload-alt drop-zone-icon"></i>
<p class="drop-zone-text">Drag &amp; drop media here</p>
<p class="drop-zone-sub">or click to browse — images, videos or gifs, several at once</p>
</div>
</div>
<input type="file" id="media-input" accept="image/*,video/*" multiple hidden>
<div class="upload-batch" id="media-batch" hidden>
<i class="fas fa-circle-notch fa-spin"></i>
<span id="media-batch-status"></span>
</div>
<div class="file-preview-grid" id="media-preview"></div>
</div>
<div class="form-group">
<label for="caption">Caption</label>
<input type="text" id="caption" name="caption" maxlength="128"
placeholder="Optional caption applied to all files" value="{{ caption }}">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary"><i class="fas fa-check"></i> Add to gallery</button>
</div>
</form>
{{ pending_uploads|json_script:'packs-pending-uploads' }}
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/asset_upload.js' %}"></script>
{% endblock %}
+89
View File
@@ -0,0 +1,89 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Browse - Packs Site{% endblock %}
{% block content %}
<div class="browse-header">
<h1><i class="fas fa-compass"></i> Browse</h1>
<form method="get" action="{% url 'library:browse' %}" class="browse-search-form">
<div class="browse-search-row">
<input type="text" name="q" value="{{ q }}" placeholder="Search packs, tags…" class="browse-search-input">
<button type="submit" class="btn btn-primary"><i class="fas fa-search"></i></button>
</div>
</form>
</div>
<div class="browse-layout">
<aside class="browse-filters">
<div class="card filter-card">
<h3>Category</h3>
<div class="filter-category">
{% for value, label, url in category_menu %}
<a href="{{ url }}" class="filter-link {% if category == value %}active{% endif %}">{{ label }}</a>
{% endfor %}
</div>
</div>
{% for group in tag_groups %}
<div class="card filter-card">
<h3>
<span class="tag-dot" style="background: {{ group.category.color }};"></span>
{{ group.category.name }}
</h3>
<div class="filter-tags">
{% for item in group.items %}
<a href="{{ item.url }}" class="tag-chip filter-tag {% if item.active %}active{% endif %}"
style="border-color: {{ item.color }}; color: {{ item.color }};">
{{ item.name }} <span class="tag-count">{{ item.count }}</span>
</a>
{% endfor %}
</div>
</div>
{% empty %}
<div class="card filter-card">
<p class="filter-empty">No tags yet. Create a project to get started!</p>
</div>
{% endfor %}
</aside>
<div class="browse-main">
<div class="browse-toolbar">
<span class="browse-count">{{ projects.paginator.count }} project{{ projects.paginator.count|pluralize }}</span>
<form method="get" action="{% url 'library:browse' %}" class="browse-sort-form">
{% if q %}<input type="hidden" name="q" value="{{ q }}">{% endif %}
{% if category %}<input type="hidden" name="category" value="{{ category }}">{% endif %}
{% for t in active_tags %}<input type="hidden" name="tag" value="{{ t }}">{% endfor %}
<select name="sort" onchange="this.form.submit()">
<option value="recent" {% if sort == 'recent' %}selected{% endif %}>Newest</option>
<option value="downloads" {% if sort == 'downloads' %}selected{% endif %}>Most downloaded</option>
<option value="name" {% if sort == 'name' %}selected{% endif %}>Name A-Z</option>
</select>
</form>
</div>
<div class="pack-grid">
{% for project in projects %}
{% include 'library/_project_card.html' %}
{% empty %}
<div class="browse-empty">
<i class="fas fa-box-open"></i>
<p>No projects match your filters.</p>
</div>
{% endfor %}
</div>
{% if projects.paginator.num_pages > 1 %}
<div class="pagination">
{% if projects.has_previous %}
<a class="btn btn-secondary" href="?{{ query_string }}&page={{ projects.previous_page_number }}">Previous</a>
{% endif %}
<span class="page-info">Page {{ projects.number }} of {{ projects.paginator.num_pages }}</span>
{% if projects.has_next %}
<a class="btn btn-secondary" href="?{{ query_string }}&page={{ projects.next_page_number }}">Next</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,86 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Contributors - {{ project.title }}{% endblock %}
{% block content %}
<h1><i class="fas fa-users"></i> Contributors — {{ project.title }}</h1>
<p><a href="{% url 'library:project_detail' project.slug %}">&larr; Back to project</a></p>
<div class="card">
<h2>Add contributor</h2>
<form method="post" class="contributor-form" id="contributor-form">
{% csrf_token %}
<input type="hidden" name="action" value="add">
<div class="form-group contributor-input-group">
{{ contributor_form.username.errors }}
<label for="{{ contributor_form.username.id_for_label }}">Username</label>
{{ contributor_form.username }}
<div class="user-suggestions" id="user-suggestions" hidden></div>
</div>
<button type="submit" class="btn btn-primary"><i class="fas fa-user-plus"></i> Add</button>
</form>
</div>
<div class="card">
<h2>Current contributors</h2>
{% if contributors %}
<ul class="contributor-list">
<li class="contributor-row">
<span class="contributor-name"><i class="fas fa-crown"></i> {{ project.owner.username }} <small>(owner)</small></span>
</li>
{% for contributor in contributors %}
<li class="contributor-row">
<span class="contributor-name"><i class="fas fa-user"></i> {{ contributor.user.username }}</span>
<form method="post" class="inline-form">
{% csrf_token %}
<input type="hidden" name="action" value="remove">
<input type="hidden" name="contributor_id" value="{{ contributor.pk }}">
<button type="submit" class="btn btn-danger btn-sm"><i class="fas fa-user-minus"></i> Remove</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}
<p class="empty-hint">No additional contributors yet.</p>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script>
(function () {
const input = document.getElementById('{{ contributor_form.username.id_for_label }}');
if (!input) return;
const suggestions = document.getElementById('user-suggestions');
let timer = null;
input.addEventListener('input', () => {
clearTimeout(timer);
const q = input.value.trim();
if (!q) { suggestions.hidden = true; return; }
timer = setTimeout(async () => {
try {
const resp = await fetch("{% url 'library:api_users_autocomplete' %}?q=" + encodeURIComponent(q), { headers: { 'Accept': 'application/json' } });
const results = await resp.json();
suggestions.innerHTML = '';
for (const u of results.slice(0, 8)) {
const item = document.createElement('button');
item.type = 'button';
item.className = 'tag-suggestion';
item.textContent = u.username;
item.addEventListener('click', () => { input.value = u.username; suggestions.hidden = true; });
suggestions.appendChild(item);
}
suggestions.hidden = results.length === 0;
} catch (e) {
suggestions.hidden = true;
}
}, 200);
});
input.addEventListener('keydown', (e) => { if (e.key === 'Escape') suggestions.hidden = true; });
document.addEventListener('click', (e) => {
if (!e.target.closest('.contributor-input-group')) suggestions.hidden = true;
});
})();
</script>
{% endblock %}
@@ -0,0 +1,122 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Create Project - Packs Site{% endblock %}
{% block content %}
<h1><i class="fas fa-plus-circle"></i> New Project</h1>
<form method="post" class="card project-form create-form" id="project-form">
{% csrf_token %}
<div class="create-grid">
<section class="create-panel create-panel-main">
<h2 class="form-section-title"><i class="fas fa-folder-open"></i> Project</h2>
<div class="form-group">
{{ project_form.title.errors }}
<label for="{{ project_form.title.id_for_label }}">Title</label>
{{ project_form.title }}
</div>
<div class="form-group">
{{ project_form.summary.errors }}
<label for="{{ project_form.summary.id_for_label }}">Summary</label>
{{ project_form.summary }}
</div>
<div class="form-group">
{{ project_form.category.errors }}
<label for="{{ project_form.category.id_for_label }}">Category</label>
{{ project_form.category }}
</div>
<div class="form-group">
{{ project_form.description.errors }}
<label for="{{ project_form.description.id_for_label }}">Description</label>
{{ project_form.description }}
<p class="bio-help"><i class="fas fa-info-circle"></i> Supports Markdown — headings, lists, links, code, tables and more.</p>
</div>
</section>
<section class="create-panel">
<h2 class="form-section-title"><i class="fas fa-image"></i> Thumbnail</h2>
<div class="upload-zone" data-upload="thumbnail">
<div class="drop-zone" role="button" tabindex="0">
<div class="drop-zone-inner">
<i class="fas fa-cloud-upload-alt drop-zone-icon"></i>
<p class="drop-zone-text">Drag &amp; drop an image</p>
<p class="drop-zone-sub">or click to browse</p>
</div>
</div>
<input type="file" id="thumb-input" accept="image/*" hidden>
<div class="upload-preview" id="thumb-preview"></div>
</div>
<h2 class="form-section-title" style="margin-top:16px;"><i class="fas fa-tags"></i> Tags</h2>
<div class="tag-editor">
<input type="text" id="tag-input" placeholder="Type a tag and press Enter… (e.g. content:threesome, species:dragon)" autocomplete="off">
<div class="tag-suggestions" id="tag-suggestions" hidden></div>
<div class="tag-chips" id="tag-chips"></div>
{{ project_form.tags }}
</div>
<p class="bio-help"><i class="fas fa-info-circle"></i> Categories: loader, version, general, creator, content, species, non_version. Unprefixed tags go to General.</p>
</section>
<section class="create-panel">
<h2 class="form-section-title"><i class="fas fa-box-open"></i> First version</h2>
<div class="form-group">
{{ version_form.version_name.errors }}
<label for="{{ version_form.version_name.id_for_label }}">{{ version_form.version_name.label }}</label>
{{ version_form.version_name }}
</div>
<div class="upload-zone" data-upload="version">
<div class="drop-zone" role="button" tabindex="0">
<div class="drop-zone-inner">
<i class="fas fa-cloud-upload-alt drop-zone-icon"></i>
<p class="drop-zone-text">Drag &amp; drop the pack file</p>
<p class="drop-zone-sub">or click to browse</p>
</div>
</div>
<input type="file" id="version-input" hidden>
<div class="upload-preview" id="version-preview"></div>
</div>
<div class="form-group">
{{ version_form.changelog.errors }}
<label for="{{ version_form.changelog.id_for_label }}">{{ version_form.changelog.label }}</label>
{{ version_form.changelog }}
<p class="bio-help"><i class="fas fa-info-circle"></i> Supports Markdown.</p>
</div>
<h2 class="form-section-title" style="margin-top:16px;"><i class="fas fa-images"></i> Gallery media</h2>
<div class="upload-zone" data-upload="media">
<div class="drop-zone" role="button" tabindex="0">
<div class="drop-zone-inner">
<i class="fas fa-cloud-upload-alt drop-zone-icon"></i>
<p class="drop-zone-text">Drag &amp; drop media</p>
<p class="drop-zone-sub">or click to browse — several at once</p>
</div>
</div>
<input type="file" id="media-input" accept="image/*,video/*" multiple hidden>
<div class="upload-batch" id="media-batch" hidden>
<i class="fas fa-circle-notch fa-spin"></i>
<span id="media-batch-status"></span>
</div>
<div class="file-preview-grid" id="media-preview"></div>
</div>
<div class="form-group">
{{ project_form.caption.errors }}
<label for="{{ project_form.caption.id_for_label }}">{{ project_form.caption.label }}</label>
{{ project_form.caption }}
</div>
</section>
</div>
<div class="form-actions">
<span class="autosave-status" id="autosave-status"><i class="fas fa-spinner fa-spin"></i></span>
<button type="submit" class="btn btn-primary" id="create-submit"><i class="fas fa-check"></i> Create project</button>
</div>
</form>
{% endblock %}
{% block extra_js %}
{{ draft_uploads|json_script:'packs-draft-uploads' }}
<script src="{% static 'js/tags.js' %}"></script>
<script src="{% static 'js/create.js' %}"></script>
{% endblock %}
@@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block title %}Delete {{ project.title }} - Packs Site{% endblock %}
{% block content %}
<div class="card">
<h1><i class="fas fa-trash"></i> Delete {{ project.title }}?</h1>
<p class="empty-hint">This permanently deletes the project, all its versions and gallery media. This cannot be undone.</p>
<form method="post">
{% csrf_token %}
<div class="form-actions">
<a href="{% url 'library:project_detail' project.slug %}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-danger"><i class="fas fa-trash"></i> Delete project</button>
</div>
</form>
</div>
{% endblock %}
@@ -0,0 +1,140 @@
{% extends 'base.html' %}
{% load static %}
{% load markdown %}
{% block title %}{{ project.title }} - Packs Site{% endblock %}
{% block content %}
<div class="project-header card">
<div class="project-avatar">
{% if project.thumbnail %}
<img src="{{ project.thumbnail_url }}" alt="{{ project.title }}">
{% else %}
<div class="default-avatar large">{{ project.title|first|upper }}</div>
{% endif %}
</div>
<div class="project-header-main">
<div class="project-title-row">
<h1>{{ project.title }}</h1>
<span class="category-badge">{{ project.get_category_display }}</span>
</div>
{% if project.summary %}<p class="project-summary">{{ project.summary }}</p>{% endif %}
<div class="project-meta">
<span class="project-owner"><i class="fas fa-user"></i>
<a href="{% url 'profiles:user_profile' project.owner.username %}">{{ project.owner.username }}</a>
</span>
<span class="project-stat"><i class="fas fa-download"></i> {{ project.downloads }}</span>
<span class="project-stat"><i class="fas fa-tag"></i> {{ versions|length }} version{{ versions|length|pluralize }}</span>
</div>
{% if can_edit %}
<div class="project-actions">
<a href="{% url 'library:version_upload' project.slug %}" class="btn btn-primary"><i class="fas fa-upload"></i> New version</a>
<a href="{% url 'library:asset_upload' project.slug %}" class="btn btn-secondary"><i class="fas fa-images"></i> Add media</a>
<a href="{% url 'library:project_edit' project.slug %}" class="btn btn-secondary"><i class="fas fa-pen"></i> Edit</a>
{% if request.user == project.owner %}
<a href="{% url 'library:contributors' project.slug %}" class="btn btn-secondary"><i class="fas fa-users"></i> Contributors</a>
<a href="{% url 'library:project_delete' project.slug %}" class="btn btn-danger"><i class="fas fa-trash"></i></a>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% if tags %}
<div class="project-tags card">
{% regroup tags by tag.category as tag_groups %}
{% for group in tag_groups %}
<div class="tag-group">
<span class="tag-group-label">
<span class="tag-dot" style="background: {{ group.grouper.color }};"></span>
{{ group.grouper.name }}
</span>
{% for link in group.list %}
<a href="{% url 'library:browse' %}?tag={{ link.tag.name }}" class="tag-chip"
style="border-color: {{ group.grouper.color }}; color: {{ group.grouper.color }};">{{ link.tag.name }}</a>
{% endfor %}
</div>
{% endfor %}
</div>
{% endif %}
<div class="tab-bar" role="tablist">
<button class="tab-btn active" data-tab="description" role="tab">Description</button>
<button class="tab-btn" data-tab="versions" role="tab">Versions</button>
<button class="tab-btn" data-tab="gallery" role="tab">Gallery</button>
</div>
<section class="tab-panel active" id="tab-description">
<div class="card">
{% if project.description %}
<div class="markdown-body project-description">{{ project.description|markdown }}</div>
{% else %}
<p class="empty-hint">No description yet.</p>
{% endif %}
</div>
</section>
<section class="tab-panel" id="tab-versions">
<div class="card">
<h2><i class="fas fa-history"></i> Versions</h2>
{% for version in versions %}
<div class="version-row">
<div class="version-info">
<div class="version-name-row">
<strong class="version-name">{{ version.version_name }}</strong>
{% if forloop.first %}<span class="latest-badge">latest</span>{% endif %}
</div>
<span class="version-date"><i class="fas fa-clock"></i> {{ version.created_at|date:"M j, Y" }}</span>
</div>
{% if version.changelog %}
<div class="markdown-body version-changelog">{{ version.changelog|markdown }}</div>
{% endif %}
<a class="btn btn-primary btn-sm" href="{% url 'library:version_download' project.slug version.pk %}">
<i class="fas fa-download"></i> {{ version.downloads }}
</a>
</div>
{% empty %}
<p class="empty-hint">No versions yet.</p>
{% endfor %}
</div>
</section>
<section class="tab-panel" id="tab-gallery">
<div class="card">
<h2><i class="fas fa-images"></i> Gallery</h2>
<div class="gallery-grid">
{% for asset in assets %}
<figure class="gallery-item">
{% if asset.is_video %}
<video controls preload="metadata" src="{{ asset.file_url }}"></video>
{% else %}
<a href="{{ asset.file_url }}" target="_blank"><img src="{{ asset.file_url }}" alt="{{ asset.caption }}"></a>
{% endif %}
{% if asset.caption %}<figcaption>{{ asset.caption }}</figcaption>{% endif %}
{% if can_edit %}
<a href="{% url 'library:asset_delete' project.slug asset.pk %}" class="gallery-delete" title="Delete media"><i class="fas fa-trash"></i></a>
{% endif %}
</figure>
{% empty %}
<p class="empty-hint">No media yet.</p>
{% endfor %}
</div>
</div>
</section>
{% endblock %}
{% block extra_js %}
<script>
(function () {
const buttons = document.querySelectorAll('.tab-btn');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
buttons.forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
});
});
})();
</script>
{% endblock %}
@@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Edit {{ project.title }} - Packs Site{% endblock %}
{% block content %}
<h1><i class="fas fa-pen"></i> Edit {{ project.title }}</h1>
<p><a href="{% url 'library:project_detail' project.slug %}">&larr; Back to project</a></p>
<form method="post" enctype="multipart/form-data" class="card project-form" id="project-form">
{% csrf_token %}
<div class="project-form-grid">
<div class="project-form-main">
{% include 'library/_project_form_fields.html' with form=project_form %}
</div>
<aside class="project-form-side">
{% include 'library/_project_form_side.html' with form=project_form %}
</aside>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary"><i class="fas fa-check"></i> Save changes</button>
</div>
</form>
{% endblock %}
{% block extra_js %}
<script src="{% static 'js/tags.js' %}"></script>
{% endblock %}
@@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}New Version - {{ project.title }}{% endblock %}
{% block content %}
<h1><i class="fas fa-upload"></i> New version — {{ project.title }}</h1>
<p><a href="{% url 'library:project_detail' project.slug %}">&larr; Back to project</a></p>
<form method="post" enctype="multipart/form-data" class="card project-form">
{% csrf_token %}
{% include 'library/_version_form_fields.html' with form=version_form %}
<div class="form-actions">
<button type="submit" class="btn btn-primary"><i class="fas fa-upload"></i> Upload version</button>
</div>
</form>
{% endblock %}
+68 -11
View File
@@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load static %}
{% load markdown %}
{% block title %}{{ profile_user.username }} - Packs{% endblock %}
@@ -38,11 +39,16 @@
{{ bio_form.bio.errors }}
{{ bio_form.bio }}
</div>
<button type="submit" class="btn btn-primary">Save bio</button>
<p class="bio-help"><i class="fas fa-info-circle"></i> Supports <a href="#" data-md-help>Markdown</a> — headings, lists, links, code, tables and more.</p>
<div class="bio-actions">
<button type="submit" class="btn btn-primary">Save bio</button>
<button type="button" class="btn btn-secondary" data-bio-toggle>Preview</button>
</div>
</form>
<div class="markdown-preview markdown-body" id="bio-preview" hidden></div>
{% else %}
{% if profile.bio %}
<p class="profile-bio">{{ profile.bio }}</p>
<div class="profile-bio markdown-body">{{ profile.bio|markdown }}</div>
{% else %}
<p class="profile-bio-empty"><i class="fas fa-user"></i> {{ profile_user.username }} hasn't written a bio yet.</p>
{% endif %}
@@ -54,16 +60,67 @@
<h2>Packs by {{ profile_user.username }}</h2>
</div>
<div class="pack-grid">
{% comment %} Placeholder — replace with real Pack objects when the library model lands. {% endcomment %}
<div class="pack-card pack-card-empty">
<div class="pack-card-thumb">
<i class="fas fa-box-open"></i>
{% for project in projects %}
{% include 'library/_project_card.html' %}
{% empty %}
<div class="pack-card pack-card-empty">
<div class="pack-card-thumb">
<i class="fas fa-box-open"></i>
</div>
<div class="pack-card-body">
<h3>No packs yet</h3>
<p class="pack-card-desc">This creator hasn't published anything yet.</p>
</div>
</div>
<div class="pack-card-body">
<h3>No packs yet</h3>
<p class="pack-card-desc">This creator hasn't published any datapacks.</p>
</div>
</div>
{% endfor %}
</div>
</section>
{% endblock %}
{% block extra_js %}
{% if is_owner %}
<script>
(function () {
const textarea = document.getElementById('{{ bio_form.bio.id_for_label }}');
const preview = document.getElementById('bio-preview');
const toggleBtn = document.querySelector('[data-bio-toggle]');
if (!textarea || !preview || !toggleBtn) return;
const url = "{% url 'profiles:bio_preview' %}";
let timer = null;
function refreshPreview() {
fetch(url, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken'),
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'bio=' + encodeURIComponent(textarea.value),
})
.then(r => r.json())
.then(data => { preview.innerHTML = data.html; })
.catch(() => { preview.innerHTML = '<p><em>Preview unavailable.</em></p>'; });
}
toggleBtn.addEventListener('click', () => {
if (preview.hidden) {
preview.hidden = false;
toggleBtn.textContent = 'Hide preview';
refreshPreview();
} else {
preview.hidden = true;
toggleBtn.textContent = 'Preview';
}
});
textarea.addEventListener('input', () => {
if (preview.hidden) return;
clearTimeout(timer);
timer = setTimeout(refreshPreview, 300);
});
})();
</script>
{% endif %}
{% endblock %}