Files
devplacepy/devplacepy/templates/project_detail.html
T
blindxfishandClaude Fable 5 8a2e0e6e70 Align the project hero with the showcase design: owner cover, logo, repo link
The hero now matches the showcase reference: the title block, tagline
(first description line), type/platform chips, author row and started
date render OVERLAID on the cover banner behind a bottom scrim, with
the optional project logo as a framed tile beside them and the Visit
Website CTA on the right. The section tab bar switches to the underline
style with Overview active.

Owners control the missing pieces from the create and edit modals,
which are now multipart: cover_image and logo_image file uploads
(stored as bare uploaded filenames via save_inline_image, the
posts.image pattern; a new upload replaces the previous one) plus a
repo_url sibling of website_url (same normalization/validation). The
cover feeds og:image and the schema image, the logo becomes schema
thumbnailUrl, and sameAs now carries website + repository. repo_url
rides ProjectOut, the Devii create/edit actions and the API docs;
tests cover repo normalization, the multipart upload round-trip and
the hero render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 22:12:25 +02:00

334 lines
22 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') }}">
<link rel="stylesheet" href="{{ static_url('/static/css/feed.css') }}">
{% endblock %}
{% block content %}
{% set project_url = "/projects/" ~ (project['slug'] or project['uid']) %}
{% set image_attachments = attachments | selectattr('is_image') | list %}
{% set other_attachments = attachments | rejectattr('is_image') | list %}
{% set cover_src = ('/static/uploads/' ~ project['cover_image']) if project.get('cover_image') else (image_attachments[0]['url'] if image_attachments else none) %}
{% set logo_src = ('/static/uploads/' ~ project['logo_image']) if project.get('logo_image') else none %}
{% set tagline = (project.get('description', '') or '').split('\n')[0] %}
<div class="project-page">
<a href="/projects" class="back-link">&larr; Back to Projects</a>
<article class="project-hero">
<div class="project-cover{% if not cover_src %} project-cover-fallback{% endif %}">
{% if cover_src %}
<img class="project-cover-img" src="{{ cover_src }}" alt="{{ project['title'] }} cover image" loading="eager">
{% endif %}
<div class="project-cover-scrim" aria-hidden="true"></div>
<div class="project-hero-overlay">
{% if logo_src %}
<img class="project-logo" src="{{ logo_src }}" alt="{{ project['title'] }} logo" loading="eager">
{% endif %}
<div class="project-hero-headline">
<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 %}">
&#x25CF; {{ project.get('status', 'In Development') }}
</div>
</div>
{% if tagline %}
<p class="project-tagline">{{ render_title(tagline, author_is_admin=is_admin(author)) }}</p>
{% endif %}
<div class="project-detail-meta project-hero-chips">
<span class="badge badge-type">{{ project.get('project_type', 'software').replace('_', ' ') }}</span>
{% for plat in platforms %}
<span class="platform-tag">{{ plat.strip() }}</span>
{% endfor %}
{% if is_private %}<span class="badge badge-type">Private</span>{% endif %}
{% if read_only %}<span class="badge badge-type">Read-only</span>{% endif %}
</div>
<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">&middot; Level {{ author.get('level', 1) if author else 1 }}</span>
</div>
{% if project.get('created_at') %}
<span class="meta-muted">&#x1F331; Started {{ dt_ago(project['created_at']) }}</span>
{% endif %}
</div>
</div>
{% if project.get('website_url') %}
<div class="project-hero-ctas">
<a href="{{ project['website_url'] }}" target="_blank" rel="noopener nofollow" class="btn btn-primary project-website-btn"><span class="icon">&#x1F310;</span> Visit Website</a>
</div>
{% endif %}
</div>
</div>
<div class="project-hero-body">
{% if project.get('release_date') or project.get('demo_date') or forked_from %}
<div class="project-detail-meta">
{% if project.get('release_date') %}
<span>&#x1F4C5; Released: {{ format_date(project['release_date']) }}</span>
{% endif %}
{% if project.get('demo_date') %}
<span>&#x1F3AD; Demo: {{ format_date(project['demo_date']) }}</span>
{% endif %}
{% if forked_from %}
<span>&#x2442; Forked from <a href="/projects/{{ forked_from['slug'] }}">{{ render_title(forked_from['title']) }}</a></span>
{% endif %}
</div>
{% endif %}
<div class="project-detail-actions">
<a href="{{ project_url }}/files" class="project-star-btn"><span class="icon">&#x1F4C1;</span><span class="label"> Files ({{ file_count }} files)</span></a>
{% if workspace_editor_url %}
<a href="{{ workspace_editor_url }}" target="_blank" rel="noopener" class="project-star-btn"><span class="icon">&#x1F4BB;</span><span class="label"> VS Code</span></a>
{% endif %}
<button type="button" class="project-star-btn" data-share="{{ project_url }}"><span class="icon">&#x1F517;</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" %}
{% set _type = "project" %}{% set _uid = project['uid'] %}{% set _owner = project['user_uid'] %}{% set _owner_name = (author or {}).get('username', '') %}{% set _class = "project-star-btn" %}{% include "_report_button.html" %}
<button type="button" class="project-star-btn project-actions-more" aria-haspopup="menu" aria-expanded="false" aria-label="More actions"><span class="icon">&#x22EF;</span><span class="label"> More</span></button>
<div class="project-actions-overflow" hidden>
{% if viewer_can_workspace %}
<a href="{{ project_url }}/workspace" data-menu-action data-menu-icon="&#x1F4BB;" data-menu-label="Workspace">Workspace</a>
{% endif %}
{% if viewer_can_containers %}
<a href="{{ project_url }}/containers" data-menu-action data-menu-icon="&#x1F5A5;&#xFE0F;" data-menu-label="Containers">Containers</a>
{% endif %}
<button type="button" data-zip-download="{{ project_url }}/zip" data-menu-action data-menu-icon="&#x1F4E6;" data-menu-label="Download zip">Download zip</button>
{% if user %}
<button type="button" data-fork-project="{{ project_url }}/fork" data-fork-name="{{ project['title'] }}" data-menu-action data-menu-icon="&#x2442;" data-menu-label="Fork">Fork</button>
{% endif %}
{% if is_owner %}
<button type="button" data-modal="edit-project-modal" data-menu-action data-menu-icon="&#x270F;&#xFE0F;" data-menu-label="Edit">Edit</button>
<form method="POST" action="{{ project_url }}/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 %}&#x1F513;{% else %}&#x1F512;{% 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="{{ project_url }}/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 %}&#x1F4DD;{% else %}&#x1F6AB;{% 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="&#x1F5D1;&#xFE0F;" data-menu-label="Delete">Delete</button>
</form>
{% endif %}
</div>
</div>
</div>
</article>
<nav class="project-tabs" aria-label="Project sections">
<a href="#about" class="project-tab active">Overview</a>
<a href="#devlog" class="project-tab">Devlog <span class="project-tab-count">{{ devlog_count }}</span></a>
{% if image_attachments %}
<a href="#screenshots" class="project-tab">Screenshots <span class="project-tab-count">{{ image_attachments | length }}</span></a>
{% endif %}
<a href="#comments" class="project-tab">Comments <span class="project-tab-count">{{ comment_count }}</span></a>
<a href="{{ project_url }}/files" class="project-tab">Files <span class="project-tab-count">{{ file_count }}</span></a>
</nav>
<div class="project-columns">
<div class="project-main">
<section class="project-about" id="about">
<h2 class="project-section-label">About</h2>
{% if maturity_hidden(maturity, user) %}
{% set _level = maturity %}{% include "_maturity_gate.html" %}
{% else %}
<div class="project-detail-desc rendered-content">{{ render_content(project.get('description', ''), author_is_admin=is_admin(author)) }}</div>
{% endif %}
{% if other_attachments %}
{% set attachments = other_attachments %}
{% include "_attachment_display.html" %}
{% endif %}
</section>
<section class="project-devlog" id="devlog">
<div class="project-devlog-header">
<h2 class="project-section-label">Devlog</h2>
<span class="project-devlog-count">{{ devlog_count }} update{{ '' if devlog_count == 1 else 's' }}</span>
{% if is_owner %}
<button type="button" class="btn btn-primary btn-sm project-devlog-post-btn" data-modal="create-post-modal"><span class="icon">&#x270D;&#xFE0F;</span> Post update</button>
{% endif %}
</div>
{% 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.{% if is_owner %} Share your first update to give this project a public build log.{% endif %}</p>
{% endif %}
</section>
{% if image_attachments %}
<section class="project-screenshots" id="screenshots">
<h2 class="project-section-label">Screenshots</h2>
<div class="project-screenshot-grid">
{% for shot in image_attachments %}
<img src="{{ shot['thumbnail_url'] or shot['url'] }}" data-lightbox data-full="{{ shot['url'] }}" alt="{{ project['title'] }} screenshot {{ loop.index }}" loading="lazy" class="project-screenshot">
{% endfor %}
</div>
</section>
{% endif %}
<section class="project-comments" id="comments">
{% with target_uid=project['uid'], target_type="project" %}
{% include "_comment_section.html" %}
{% endwith %}
</section>
</div>
<aside class="project-sidebar">
<div class="project-sidebar-card">
<h2 class="project-section-label">Links</h2>
<ul class="project-link-list">
{% if project.get('website_url') %}
<li><a href="{{ project['website_url'] }}" target="_blank" rel="noopener nofollow">&#x1F310; Website</a></li>
{% endif %}
{% if project.get('repo_url') %}
<li><a href="{{ project['repo_url'] }}" target="_blank" rel="noopener nofollow">&#x1F4BB; Repository</a></li>
{% endif %}
<li><a href="{{ project_url }}/files">&#x1F4C1; Browse files</a></li>
{% if forked_from %}
<li><a href="/projects/{{ forked_from['slug'] }}">&#x2442; Fork source: {{ render_title(forked_from['title']) }}</a></li>
{% endif %}
</ul>
</div>
<div class="project-sidebar-card">
<h2 class="project-section-label">Stats</h2>
<div class="project-stats">
<span class="project-stat"><span class="project-stat-value">{{ star_count }}</span> stars</span>
<span class="project-stat"><a href="#devlog"><span class="project-stat-value">{{ devlog_count }}</span> update{{ '' if devlog_count == 1 else 's' }}</a></span>
<span class="project-stat"><a href="#comments"><span class="project-stat-value">{{ comment_count }}</span> comment{{ '' if comment_count == 1 else 's' }}</a></span>
<span class="project-stat"><a href="{{ project_url }}/files"><span class="project-stat-value">{{ file_count }}</span> file{{ '' if file_count == 1 else 's' }}</a></span>
<span class="project-stat"><span class="project-stat-value">{{ fork_count }}</span> fork{{ '' if fork_count == 1 else 's' }}</span>
</div>
{% if devlog_posts %}
<div class="project-last-update">Last update {{ dt_ago(devlog_posts[0].post['created_at']) }}</div>
{% endif %}
</div>
<div class="project-sidebar-card">
<h2 class="project-section-label">Author</h2>
<div class="project-author-card">
{% set _size = 40 %}{% set _size_class = "md" %}{% set _user = author %}{% include "_avatar_link.html" %}
<div class="project-author-meta">
{% set _user = author %}{% set _class = none %}{% include "_user_link.html" %}
<span class="meta-muted">Level {{ author.get('level', 1) if author else 1 }} &middot; {{ author.get('stars', 0) if author else 0 }} stars</span>
</div>
</div>
</div>
</aside>
</div>
{% if is_owner %}
{% call modal('create-post-modal', 'Post an update') %}
{% set _composer_topic = 'devlog' %}{% set _composer_project = project['uid'] %}{% include "_post_composer_form.html" %}
{% endcall %}
{% call modal('edit-project-modal', 'Edit Project') %}
<form method="POST" action="/projects/edit/{{ project['slug'] or project['uid'] }}" enctype="multipart/form-data">
<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="grid-2col">
<div class="auth-field">
<label for="edit-project-website_url">Website (optional)</label>
<input type="url" id="edit-project-website_url" name="website_url" maxlength="500" placeholder="https://myproject.dev" value="{{ project.get('website_url', '') or '' }}">
</div>
<div class="auth-field">
<label for="edit-project-repo_url">Repository (optional)</label>
<input type="url" id="edit-project-repo_url" name="repo_url" maxlength="500" placeholder="https://github.com/me/project" value="{{ project.get('repo_url', '') or '' }}">
</div>
</div>
<div class="grid-2col">
<div class="auth-field">
<label for="edit-project-cover_image">Cover image (optional{% if project.get('cover_image') %}, replaces current{% endif %})</label>
<input type="file" id="edit-project-cover_image" name="cover_image" accept="image/*">
</div>
<div class="auth-field">
<label for="edit-project-logo_image">Project logo (optional{% if project.get('logo_image') %}, replaces current{% endif %})</label>
<input type="file" id="edit-project-logo_image" name="logo_image" accept="image/*">
</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">&#x1F4BE;</span>Save Changes</button>
</div>
</form>
{% endcall %}
{% endif %}
</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 %}