fixed implementation of UGC content uploader
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<p>Hosted datapacks for Needs Of Nature & 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/<user>/<pack>/.</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 %}">← 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 & 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 %}
|
||||
@@ -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 %}">← 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 & 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 & 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 & 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 %}">← 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 %}">← 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 %}
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user