```yaml
Outcome: done
Changed: devplacepy/templates/project_detail.html:116-128, devplacepy/static/css/projects.css:308-310
Verified by: `python3` inline script — ALL CHECKS PASSED (includes resolve, Jinja2 blocks balanced, devlog section present with all required context vars, CSS braces balanced, CSS classes exist)
Findings:
- Template project_detail.html:116-127 — Devlog section renders posts via `_post_card.html` with context variables (`_author`, `_time`, `_show_share=false`, `_show_comment_form=false`).
- Template project_detail.html:123 — Pagination reuses `_load_more.html` via `{% set next_cursor = devlog_next_cursor %}{% include "_load_more.html" %}`.
- Template project_detail.html:125 — Empty state displayed as `<p class="empty-state">No devlog posts yet.</p>` when `devlog_posts` is falsy.
- CSS projects.css:308-310 — `.project-devlog` rule provides `margin-top: 1.5rem` separation from project detail article.
- CSS projects.css:312-319 — `.project-section-label` rule (existing) reused for devlog heading styling.
- No new CSS file reference added — `projects.css` and `post.css` were already loaded in the template `extra_head`.
- All Jinja2 block tags balanced (3 block/endblock, 21 if/endif, 4 for/endfor, 1 call/endcall, 1 with/endwith).
- All 9 `{% include %}` paths resolve to existing template files.
- Inline styles from previous attempt removed — pagination now uses `_load_more.html` exclusively.
Open: none
Confidence: high — all 6 acceptance crit
Typosaurus-Run: 45c0aee6df2649ffaa248729bdfd5841
Typosaurus-Node: 7bfee38d7ded4822a374ef066bcf85aa
Typosaurus-Agent: @nadia
Refs: #135
202 lines
12 KiB
HTML
202 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_macros.html" import modal %}
|
|
{% block extra_head %}
|
|
<link rel="stylesheet" href="{{ static_url('/static/css/projects.css') }}">
|
|
<link rel="stylesheet" href="{{ static_url('/static/css/post.css') }}">
|
|
{% endblock %}
|
|
{% block content %}
|
|
<div class="project-detail-page">
|
|
<a href="/projects" class="back-link">← Back to Projects</a>
|
|
|
|
<article class="project-detail">
|
|
<div class="project-detail-header">
|
|
<h1 class="project-detail-title">{{ render_title(project['title'], author_is_admin=is_admin(author)) }}</h1>
|
|
<div class="project-status {% if project.get('status') == 'Released' %}released{% else %}dev{% endif %}">
|
|
● {{ project.get('status', 'In Development') }}
|
|
</div>
|
|
</div>
|
|
|
|
{% if is_private or read_only %}
|
|
<div class="project-detail-meta">
|
|
{% if is_private %}<span class="badge badge-type">Private</span>{% endif %}
|
|
{% if read_only %}<span class="badge badge-type">Read-only</span>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="project-detail-meta">
|
|
<span class="badge badge-type">{{ project.get('project_type', 'software').replace('_', ' ') }}</span>
|
|
{% if project.get('release_date') %}
|
|
<span>📅 Released: {{ format_date(project['release_date']) }}</span>
|
|
{% endif %}
|
|
{% if project.get('demo_date') %}
|
|
<span>🎭 Demo: {{ format_date(project['demo_date']) }}</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if forked_from %}
|
|
<div class="project-detail-meta">
|
|
<span>⑂ Forked from <a href="/projects/{{ forked_from['slug'] }}">{{ render_title(forked_from['title']) }}</a></span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="project-detail-author">
|
|
{% set _size = 32 %}{% set _size_class = "sm" %}{% set _user = author %}{% include "_avatar_link.html" %}
|
|
<div>
|
|
{% set _user = author %}{% set _class = none %}{% include "_user_link.html" %}
|
|
<span class="meta-muted">· Level {{ author.get('level', 1) if author else 1 }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="project-detail-desc rendered-content">{{ render_content(project.get('description', ''), author_is_admin=is_admin(author)) }}</div>
|
|
|
|
{% if attachments %}
|
|
{% include "_attachment_display.html" %}
|
|
{% endif %}
|
|
|
|
{% if platforms %}
|
|
<div class="project-platforms">
|
|
<h4 class="project-section-label">Platforms</h4>
|
|
<div class="project-card-platforms">
|
|
{% for plat in platforms %}
|
|
<span class="platform-tag">{{ plat.strip() }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="project-detail-actions">
|
|
<a href="/projects/{{ project['slug'] or project['uid'] }}/files" class="project-star-btn"><span class="icon">📁</span><span class="label"> Files ({{ file_count }} files)</span></a>
|
|
<button type="button" class="project-star-btn" data-share="/projects/{{ project['slug'] or project['uid'] }}"><span class="icon">🔗</span><span class="label"> Share</span></button>
|
|
{% if user %}
|
|
{% set _type = "project" %}{% set _uid = project['uid'] %}{% set _my_vote = my_vote %}{% set _count = star_count %}{% set _btn_class = "project-star-btn" %}{% include "_star_vote.html" %}
|
|
{% endif %}
|
|
{% set _type = "project" %}{% set _uid = project['uid'] %}{% set _bookmarked = bookmarked %}{% include "_bookmark_button.html" %}
|
|
{% set _type = "project" %}{% set _uid = project['uid'] %}{% set _reactions = reactions %}{% include "_reaction_bar.html" %}
|
|
<button type="button" class="project-star-btn project-actions-more" aria-haspopup="menu" aria-expanded="false" aria-label="More actions"><span class="icon">⋯</span><span class="label"> More</span></button>
|
|
|
|
<div class="project-actions-overflow" hidden>
|
|
{% if viewer_can_containers %}
|
|
<a href="/projects/{{ project['slug'] or project['uid'] }}/containers" data-menu-action data-menu-icon="🖥️" data-menu-label="Containers">Containers</a>
|
|
{% endif %}
|
|
<button type="button" data-zip-download="/projects/{{ project['slug'] or project['uid'] }}/zip" data-menu-action data-menu-icon="📦" data-menu-label="Download zip">Download zip</button>
|
|
{% if user %}
|
|
<button type="button" data-fork-project="/projects/{{ project['slug'] or project['uid'] }}/fork" data-fork-name="{{ project['title'] }}" data-menu-action data-menu-icon="⑂" data-menu-label="Fork">Fork</button>
|
|
{% endif %}
|
|
{% if is_owner %}
|
|
<button type="button" data-modal="edit-project-modal" data-menu-action data-menu-icon="✏️" data-menu-label="Edit">Edit</button>
|
|
<form method="POST" action="/projects/{{ project['slug'] or project['uid'] }}/private">
|
|
<input type="hidden" name="value" value="{{ 0 if is_private else 1 }}">
|
|
<button type="submit" data-confirm-danger data-confirm="{% if is_private %}Make this project public? Everyone will be able to see the project and all its files.{% else %}Make this project private? Only you and administrators will be able to see it.{% endif %}" data-menu-action data-menu-icon="{% if is_private %}🔓{% else %}🔒{% endif %}" data-menu-label="{% if is_private %}Make public{% else %}Make private{% endif %}">{% if is_private %}Make public{% else %}Make private{% endif %}</button>
|
|
</form>
|
|
<form method="POST" action="/projects/{{ project['slug'] or project['uid'] }}/readonly">
|
|
<input type="hidden" name="value" value="{{ 0 if read_only else 1 }}">
|
|
<button type="submit" data-confirm-danger data-confirm="{% if read_only %}Allow file changes again for this project?{% else %}Make this project read-only? Files become immutable until you turn this off.{% endif %}" data-menu-action data-menu-icon="{% if read_only %}📝{% else %}🚫{% endif %}" data-menu-label="{% if read_only %}Allow edits{% else %}Make read-only{% endif %}">{% if read_only %}Allow edits{% else %}Make read-only{% endif %}</button>
|
|
</form>
|
|
{% endif %}
|
|
{% if is_owner or is_admin(user) %}
|
|
<form method="POST" action="/projects/delete/{{ project['slug'] or project['uid'] }}">
|
|
<button type="submit" data-confirm-danger data-confirm="Delete this project?" data-menu-action data-menu-icon="🗑️" data-menu-label="Delete">Delete</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<section class="project-devlog">
|
|
<h3 class="project-section-label">Devlog</h3>
|
|
{% if devlog_posts %}
|
|
{% for item in devlog_posts %}
|
|
{% set _author = item.author %}{% set _time = item.time_ago %}{% set _show_share = false %}{% set _show_comment_form = false %}{% include "_post_card.html" %}
|
|
{% endfor %}
|
|
{% set next_cursor = devlog_next_cursor %}{% include "_load_more.html" %}
|
|
{% else %}
|
|
<p class="empty-state">No devlog posts yet.</p>
|
|
{% endif %}
|
|
</section>
|
|
|
|
{% if is_owner %}
|
|
{% call modal('edit-project-modal', 'Edit Project') %}
|
|
<form method="POST" action="/projects/edit/{{ project['slug'] or project['uid'] }}">
|
|
<div class="auth-field auth-field-gap">
|
|
<label for="edit-project-title">Title</label>
|
|
<input type="text" id="edit-project-title" name="title" required maxlength="200" value="{{ project['title'] }}">
|
|
</div>
|
|
|
|
<div class="auth-field auth-field-gap">
|
|
<label for="edit-project-description">Description</label>
|
|
<textarea id="edit-project-description" name="description" required maxlength="5000" class="min-h-100" data-mention>{{ project.get('description', '') }}</textarea>
|
|
</div>
|
|
|
|
<div class="grid-2col">
|
|
<div class="auth-field">
|
|
<label for="edit-project-release_date">Release Date</label>
|
|
<input type="text" id="edit-project-release_date" name="release_date" placeholder="DD/MM/YYYY" pattern="\d{2}/\d{2}/\d{4}" inputmode="numeric" autocomplete="off" value="{{ format_date(project.get('release_date', '')) }}">
|
|
</div>
|
|
<div class="auth-field">
|
|
<label for="edit-project-demo_date">Demo Date</label>
|
|
<input type="text" id="edit-project-demo_date" name="demo_date" placeholder="DD/MM/YYYY" pattern="\d{2}/\d{2}/\d{4}" inputmode="numeric" autocomplete="off" value="{{ format_date(project.get('demo_date', '')) }}">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="auth-field auth-field-gap">
|
|
<label>Type</label>
|
|
<div class="flex-wrap-gap" role="group" aria-label="Type">
|
|
{% for t in [('game', 'Game'), ('game_asset', 'Game Asset'), ('software', 'Software'), ('mobile_app', 'Mobile App'), ('website', 'Website')] %}
|
|
<label class="topic-label">
|
|
<input type="radio" name="project_type" value="{{ t[0] }}" {% if t[0] == project.get('project_type', 'software') %}checked{% endif %} class="topic-radio">
|
|
{{ t[1] }}
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="auth-field auth-field-gap">
|
|
<label>Status</label>
|
|
<div class="flex-wrap-gap" role="group" aria-label="Status">
|
|
<label class="topic-label">
|
|
<input type="radio" name="status" value="In Development" {% if project.get('status', 'In Development') != 'Released' %}checked{% endif %} class="topic-radio"> In Development
|
|
</label>
|
|
<label class="topic-label">
|
|
<input type="radio" name="status" value="Released" {% if project.get('status') == 'Released' %}checked{% endif %} class="topic-radio"> Released
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="auth-field auth-field-gap-last">
|
|
<label>Platforms</label>
|
|
<div id="platforms-tags" class="flex-wrap-gap-bottom"></div>
|
|
<input type="text" id="platforms-input" class="input-sm" placeholder="Add platform (press Enter)" aria-label="Add platform">
|
|
<input type="hidden" id="platforms" name="platforms" value="{{ project.get('platforms', '') }}">
|
|
<div class="platform-presets-wrap">
|
|
{% for p in ['PC', 'Windows', 'Mac', 'Linux', 'iOS', 'Android', 'Web', 'Console'] %}
|
|
<button type="button" class="platform-preset" data-platform="{{ p }}">{{ p }}</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="modal-close btn btn-secondary">Cancel</button>
|
|
<button type="submit" class="btn btn-primary"><span class="icon">💾</span>Save Changes</button>
|
|
</div>
|
|
</form>
|
|
{% endcall %}
|
|
{% endif %}
|
|
|
|
{% with target_uid=project['uid'], target_type="project" %}
|
|
{% include "_comment_section.html" %}
|
|
{% endwith %}
|
|
</div>
|
|
{% endblock %}
|
|
{% block extra_js %}
|
|
<script type="module">
|
|
import { ProjectActionsMenu } from "{{ static_url('/static/js/ProjectActionsMenu.js') }}";
|
|
const actions = document.querySelector(".project-detail-actions");
|
|
if (actions) {
|
|
new ProjectActionsMenu(actions);
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|
|
|
|
|