|
{% 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 cover = attachments | selectattr('uid', 'equalto', project.get('cover_attachment_uid', '')) | selectattr('is_image') | list | first %}
|
|
{% set logo = attachments | selectattr('uid', 'equalto', project.get('logo_attachment_uid', '')) | selectattr('is_image') | list | first %}
|
|
{% set hero_uids = [(cover or {}).get('uid'), (logo or {}).get('uid')] %}
|
|
{% set gallery = attachments | selectattr('is_image') | rejectattr('uid', 'in', hero_uids) | list %}
|
|
{% set other_attachments = attachments | rejectattr('is_image') | list %}
|
|
{% set cover_src = (cover or {}).get('url') or ((gallery | first or {}).get('url')) %}
|
|
<div class="project-page">
|
|
<a href="/projects" class="back-link">← Back to Projects</a>
|
|
|
|
<div class="project-shell">
|
|
<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 %}
|
|
<img class="project-logo" src="{{ logo['thumbnail_url'] or logo['url'] }}" 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 %}">
|
|
● {{ project.get('status', 'In Development') }}
|
|
</div>
|
|
</div>
|
|
<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">· Level {{ author.get('level', 1) if author else 1 }}</span>
|
|
</div>
|
|
{% if project.get('created_at') %}
|
|
<span class="meta-muted">🌱 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"><span class="icon">🌐</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>📅 Released: {{ format_date(project['release_date']) }}</span>
|
|
{% endif %}
|
|
{% if project.get('demo_date') %}
|
|
<span>🎭 Demo: {{ format_date(project['demo_date']) }}</span>
|
|
{% endif %}
|
|
{% if forked_from %}
|
|
<span>⑂ 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">📁</span><span class="label"> Files ({{ file_count }} files)</span></a>
|
|
{% if workspace_editor_url %}
|
|
{% set _url = workspace_editor_url %}
|
|
{% set _uid = project['uid'] %}
|
|
{% set _class = "project-star-btn" %}
|
|
{% set _icon = "💻"|safe %}
|
|
{% set _mode = workspace_editor_mode %}
|
|
{% set _width = workspace_editor_width %}
|
|
{% set _height = workspace_editor_height %}
|
|
{% set _label = "Editor" %}
|
|
{% include "_editor_open.html" %}
|
|
{% endif %}
|
|
<button type="button" class="project-star-btn" data-share="{{ project_url }}"><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" %}
|
|
{% 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">⋯</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="💻" data-menu-label="Workspace">Workspace</a>
|
|
{% endif %}
|
|
{% if viewer_can_containers %}
|
|
<a href="{{ project_url }}/containers" data-menu-action data-menu-icon="🖥️" data-menu-label="Containers">Containers</a>
|
|
{% endif %}
|
|
<button type="button" data-zip-download="{{ project_url }}/zip" data-menu-action data-menu-icon="📦" 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="⑂" 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>
|
|
<button type="button" data-modal="add-screenshots-modal" data-menu-action data-menu-icon="🖼️" data-menu-label="Add screenshots">Add screenshots</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 %}🔓{% 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="{{ 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 %}📝{% 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>
|
|
</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 gallery %}
|
|
<a href="#screenshots" class="project-tab">Screenshots <span class="project-tab-count">{{ gallery | 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">✍️</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 gallery %}
|
|
<section class="project-screenshots" id="screenshots">
|
|
<h2 class="project-section-label">Screenshots</h2>
|
|
<div class="project-screenshot-grid">
|
|
{% for shot in gallery[:12] %}
|
|
<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>
|
|
{% if gallery | length > 12 %}
|
|
<p class="project-screenshot-more">Showing 12 of {{ gallery | length }} screenshots.</p>
|
|
{% endif %}
|
|
</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">🌐 Website</a></li>
|
|
{% endif %}
|
|
{% if project.get('repo_url') %}
|
|
<li><a href="{{ project['repo_url'] }}" target="_blank" rel="noopener nofollow">💻 Repository</a></li>
|
|
{% endif %}
|
|
<li><a href="{{ project_url }}/files">📁 Browse files</a></li>
|
|
{% if forked_from %}
|
|
<li><a href="/projects/{{ forked_from['slug'] }}">⑂ 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 }} · {{ author.get('stars', 0) if author else 0 }} stars</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</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('add-screenshots-modal', 'Add screenshots') %}
|
|
<form method="POST" action="{{ project_url }}/screenshots">
|
|
<div class="auth-field auth-field-gap">
|
|
<label>Upload images</label>
|
|
{% include "_attachment_form.html" %}
|
|
<small class="hint-text">Images appear in the Screenshots gallery; other files list under About.</small>
|
|
</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>Add screenshots</button>
|
|
</div>
|
|
</form>
|
|
{% endcall %}
|
|
|
|
{% 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="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>Cover image{% if cover %} (replaces current){% endif %}</label>
|
|
<dp-upload name="cover_attachment_uid" max-files="1" accept="image/*" allowed-types="{{ allowed_image_types() }}" max-size="{{ max_upload_size_mb() }}" label="Upload cover"></dp-upload>
|
|
</div>
|
|
<div class="auth-field">
|
|
<label>Project logo{% if logo %} (replaces current){% endif %}</label>
|
|
<dp-upload name="logo_attachment_uid" max-files="1" accept="image/*" allowed-types="{{ allowed_image_types() }}" max-size="{{ max_upload_size_mb() }}" label="Upload logo"></dp-upload>
|
|
</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 %}
|
|
</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 %}
|