| 1 |
1 |
|
{% extends "base.html" %}
|
| 2 |
2 |
|
|
| 3 |
3 |
|
{% block title %}{{ project.title }} - {{ creator_username }}{% endblock %}
|
| 4 |
|
- |
{% block body_attrs %} class="padded-page"{% endblock %}
|
|
4 |
+ |
{% block body_attrs %} class="padded-page project-page"{% endblock %}
|
| 5 |
5 |
|
|
| 6 |
6 |
|
{% block head %}
|
| 7 |
7 |
|
<meta property="og:title" content="{{ project.title }} by {{ creator_username }}">
|
| 41 |
41 |
|
}
|
| 42 |
42 |
|
</script>
|
| 43 |
43 |
|
<link rel="alternate" type="application/rss+xml" title="{{ project.title }} - RSS Feed" href="/p/{{ project.slug }}/rss">
|
| 44 |
|
- |
<style>
|
| 45 |
|
- |
.store-header { margin-bottom: 3rem; }
|
| 46 |
|
- |
.store-title { font-size: 3rem; margin-bottom: 0.5rem; }
|
| 47 |
|
- |
.store-meta { font-size: 0.9rem; opacity: 0.7; margin-bottom: 1.5rem; }
|
| 48 |
|
- |
.store-meta a { color: var(--detail); }
|
| 49 |
|
- |
.store-description { font-size: 1.1rem; max-width: 800px; margin-bottom: 2rem; text-align: left; }
|
| 50 |
|
- |
.store-actions { display: flex; gap: 1rem; flex-wrap: wrap; }
|
| 51 |
|
- |
.items-section { margin-bottom: 3rem; }
|
| 52 |
|
- |
.items-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; }
|
| 53 |
|
- |
.item-card { background: var(--light-background); transition: background 0.2s ease; cursor: pointer; display: flex; flex-direction: column; height: 100%; }
|
| 54 |
|
- |
.item-card:hover { background: var(--surface-muted); }
|
| 55 |
|
- |
a.item-thumbnail { width: 100%; height: 300px; background: var(--border); display: flex; align-items: center; justify-content: center; font-size: 3rem; opacity: 0.3; text-decoration: none; }
|
| 56 |
|
- |
a.item-thumbnail:hover { opacity: 0.5; }
|
| 57 |
|
- |
.item-content { padding: 1.5rem; display: flex; flex-direction: column; flex-grow: 1; }
|
| 58 |
|
- |
.item-title { font-size: 1.2rem; margin-bottom: 0.5rem; font-family: var(--font-heading); font-weight: bold; }
|
| 59 |
|
- |
.item-title a { color: inherit; text-decoration: none; }
|
| 60 |
|
- |
.item-title a:hover { text-decoration: underline; }
|
| 61 |
|
- |
.item-meta { font-size: 0.85rem; opacity: 0.7; margin-bottom: 1rem; }
|
| 62 |
|
- |
.item-tags { margin-bottom: 1rem; }
|
| 63 |
|
- |
.item-description { font-size: 0.9rem; margin-bottom: 1rem; opacity: 0.8; text-align: left; flex-grow: 1; }
|
| 64 |
|
- |
.item-footer { display: flex; justify-content: space-between; align-items: center; margin-top: auto; }
|
| 65 |
|
- |
.item-price { font-size: 1.3rem; }
|
| 66 |
|
- |
.item-stats { font-size: 0.85rem; opacity: 0.6; }
|
| 67 |
|
- |
.store-footer { margin-top: 4rem; padding-top: 2rem; border-top: 1px solid var(--border); opacity: 0.6; }
|
| 68 |
|
- |
.store-footer a { color: var(--detail); }
|
| 69 |
|
- |
.item-content button.primary { width: 100%; margin-top: 1rem; }
|
| 70 |
|
- |
.item-content button.secondary { width: 100%; margin-top: 1rem; }
|
| 71 |
|
- |
.project-sections { background: var(--light-background); padding: 2rem; margin-bottom: 2rem; }
|
| 72 |
|
- |
.section-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 1.5rem; flex-wrap: wrap; }
|
| 73 |
|
- |
.section-tab { background: none; border: none; padding: 0.6rem 1.2rem; cursor: pointer; font-size: 0.95rem; opacity: 0.6; border-bottom: 2px solid transparent; font-family: var(--font-body); color: var(--text); }
|
| 74 |
|
- |
.section-tab.active { opacity: 1; border-bottom-color: var(--detail); }
|
| 75 |
|
- |
.section-tab:hover { opacity: 0.9; }
|
| 76 |
|
- |
.section-panel { display: none; }
|
| 77 |
|
- |
.section-panel.active { display: block; }
|
| 78 |
|
- |
.section-panel p { margin-bottom: 1rem; }
|
| 79 |
|
- |
.section-panel ul, .section-panel ol { margin-left: 1.5rem; margin-bottom: 1rem; }
|
| 80 |
|
- |
.section-panel li { margin-bottom: 0.5rem; }
|
| 81 |
|
- |
.tiers-section { margin-bottom: 3rem; }
|
| 82 |
|
- |
.tiers-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; }
|
| 83 |
|
- |
.tier-card { background: var(--light-background); padding: 2rem; display: flex; flex-direction: column; }
|
| 84 |
|
- |
.tier-name { font-family: var(--font-heading); font-weight: bold; font-size: 1.3rem; margin-bottom: 0.5rem; }
|
| 85 |
|
- |
.tier-price { font-size: 1.5rem; margin-bottom: 1rem; }
|
| 86 |
|
- |
.tier-description { font-size: 0.9rem; opacity: 0.8; margin-bottom: 1.5rem; flex-grow: 1; text-align: left; }
|
| 87 |
|
- |
.tier-card form { margin-top: auto; }
|
| 88 |
|
- |
.tier-card button { width: 100%; }
|
| 89 |
|
- |
.tier-active-badge { font-size: 0.85rem; padding: 0.5rem 1rem; background: var(--surface-muted); text-align: center; opacity: 0.7; }
|
| 90 |
|
- |
@media (max-width: 480px) {
|
| 91 |
|
- |
.store-title { font-size: 2rem; }
|
| 92 |
|
- |
.items-grid { grid-template-columns: 1fr; }
|
| 93 |
|
- |
a.item-thumbnail { height: 200px; }
|
| 94 |
|
- |
.store-description { font-size: 1rem; }
|
| 95 |
|
- |
.tiers-grid { grid-template-columns: 1fr; }
|
| 96 |
|
- |
}
|
| 97 |
|
- |
</style>
|
| 98 |
44 |
|
{% endblock %}
|
| 99 |
45 |
|
|
| 100 |
46 |
|
{% block content %}
|
| 103 |
49 |
|
<div class="container">
|
| 104 |
50 |
|
<header class="store-header">
|
| 105 |
51 |
|
{% if let Some(img_url) = project.cover_image_url %}
|
| 106 |
|
- |
<img src="{{ img_url }}" alt="{{ project.title }}"
|
| 107 |
|
- |
style="width: 120px; height: 120px; border-radius: 8px; object-fit: cover; margin-bottom: 1.5rem;">
|
|
52 |
+ |
<img src="{{ img_url }}" alt="{{ project.title }}" class="store-cover">
|
| 108 |
53 |
|
{% endif %}
|
| 109 |
54 |
|
<h1 class="store-title">{{ project.title }}<span class="dot">.</span></h1>
|
| 110 |
55 |
|
<div class="store-meta">
|
| 114 |
59 |
|
<p class="store-description">{{ project.description }}</p>
|
| 115 |
60 |
|
<div class="store-actions">
|
| 116 |
61 |
|
{% if is_owner %}
|
| 117 |
|
- |
<a href="/dashboard/project/{{ project.slug }}" class="secondary" style="display: inline-block; padding: 0.5rem 1rem; text-decoration: none;">Edit Project</a>
|
|
62 |
+ |
<a href="/dashboard/project/{{ project.slug }}" class="secondary">Edit Project</a>
|
| 118 |
63 |
|
{% endif %}
|
| 119 |
64 |
|
{% if session_user.is_some() %}
|
| 120 |
65 |
|
{% if is_following %}
|
| 121 |
|
- |
<button class="secondary follow-btn"
|
|
66 |
+ |
<button class="secondary follow-btn is-following"
|
| 122 |
67 |
|
hx-delete="/api/follow/project/{{ project_id }}"
|
| 123 |
|
- |
hx-swap="outerHTML"
|
| 124 |
|
- |
style="opacity: 0.7;">Following ({{ follower_count }})</button>
|
|
68 |
+ |
hx-swap="outerHTML">Following ({{ follower_count }})</button>
|
| 125 |
69 |
|
{% else %}
|
| 126 |
70 |
|
<button class="secondary follow-btn"
|
| 127 |
71 |
|
hx-post="/api/follow/project/{{ project_id }}"
|
| 128 |
72 |
|
hx-swap="outerHTML">Follow ({{ follower_count }})</button>
|
| 129 |
73 |
|
{% endif %}
|
| 130 |
74 |
|
{% else if follower_count > 0 %}
|
| 131 |
|
- |
<span style="font-size: 0.85rem; opacity: 0.7; padding: 0.5rem 0;">{{ follower_count }} followers</span>
|
|
75 |
+ |
<span class="follower-count-muted">{{ follower_count }} followers</span>
|
| 132 |
76 |
|
{% endif %}
|
| 133 |
|
- |
<a href="/p/{{ project.slug }}/rss" class="secondary" style="display: inline-block; padding: 0.5rem 1rem; text-decoration: none;">RSS Feed</a>
|
|
77 |
+ |
<a href="/p/{{ project.slug }}/rss" class="secondary">RSS Feed</a>
|
| 134 |
78 |
|
{% if has_blog_posts %}
|
| 135 |
|
- |
<a href="/p/{{ project.slug }}/blog" class="secondary" style="display: inline-block; padding: 0.5rem 1rem; text-decoration: none;">Blog</a>
|
|
79 |
+ |
<a href="/p/{{ project.slug }}/blog" class="secondary">Blog</a>
|
| 136 |
80 |
|
{% endif %}
|
| 137 |
81 |
|
{% for repo in &git_repos %}
|
| 138 |
|
- |
<a href="{{ repo.1 }}" class="secondary" style="display: inline-block; padding: 0.5rem 1rem; text-decoration: none;">Git ({{ repo.0 }})</a>
|
|
82 |
+ |
<a href="{{ repo.1 }}" class="secondary">Git ({{ repo.0 }})</a>
|
| 139 |
83 |
|
{% endfor %}
|
| 140 |
84 |
|
{% if let Some(url) = community_url %}
|
| 141 |
|
- |
<a href="{{ url }}" class="secondary" style="display: inline-block; padding: 0.5rem 1rem; text-decoration: none;">Community</a>
|
|
85 |
+ |
<a href="{{ url }}" class="secondary">Community</a>
|
| 142 |
86 |
|
{% endif %}
|
| 143 |
87 |
|
{% include "partials/tip_button.html" %}
|
| 144 |
88 |
|
</div>
|
| 148 |
92 |
|
<section class="project-sections">
|
| 149 |
93 |
|
<div class="section-tabs">
|
| 150 |
94 |
|
{% for section in sections %}
|
| 151 |
|
- |
<button class="section-tab{% if loop.first %} active{% endif %}"
|
|
95 |
+ |
<button class="section-tab{% if loop.first %} is-selected{% endif %}"
|
| 152 |
96 |
|
data-tab="section-{{ section.slug }}"
|
| 153 |
97 |
|
onclick="switchSectionTab(this, 'section-{{ section.slug }}')">{{ section.title }}</button>
|
| 154 |
98 |
|
{% endfor %}
|
| 161 |
105 |
|
</section>
|
| 162 |
106 |
|
<script>
|
| 163 |
107 |
|
function switchSectionTab(btn, panelId) {
|
| 164 |
|
- |
document.querySelectorAll('.project-sections .section-tab').forEach(function(t) { t.classList.remove('active'); });
|
|
108 |
+ |
// Tab selection uses .is-selected (charter); panel visibility
|
|
109 |
+ |
// still uses .active (display: none/block toggle).
|
|
110 |
+ |
document.querySelectorAll('.project-sections .section-tab').forEach(function(t) { t.classList.remove('is-selected'); });
|
| 165 |
111 |
|
document.querySelectorAll('.project-sections .section-panel').forEach(function(p) { p.classList.remove('active'); });
|
| 166 |
|
- |
btn.classList.add('active');
|
|
112 |
+ |
btn.classList.add('is-selected');
|
| 167 |
113 |
|
var panel = document.getElementById(panelId);
|
| 168 |
114 |
|
if (panel) panel.classList.add('active');
|
| 169 |
115 |
|
history.replaceState(null, '', '#' + panelId);
|
| 184 |
130 |
|
<h2 class="section-header">Available Items</h2>
|
| 185 |
131 |
|
<div class="view-controls">
|
| 186 |
132 |
|
<button type="button" class="view-btn" data-view="list" title="List view">|||</button>
|
| 187 |
|
- |
<button type="button" class="view-btn active" data-view="grid" title="Grid view">:::</button>
|
|
133 |
+ |
<button type="button" class="view-btn is-selected" data-view="grid" title="Grid view">:::</button>
|
| 188 |
134 |
|
</div>
|
| 189 |
135 |
|
</div>
|
| 190 |
136 |
|
<div class="items-container items-grid-view" id="items-container">
|
| 197 |
143 |
|
<div class="item-meta">{{ item.item_type }}{% if item.bundle_item_count > 0 %} ({{ item.bundle_item_count }} items){% endif %} · {{ item.release_date }}</div>
|
| 198 |
144 |
|
<div class="item-tags">
|
| 199 |
145 |
|
{% for tag in item.tags %}
|
| 200 |
|
- |
<a href="/discover?tag={{ tag.slug }}" class="tag" style="text-decoration: none; color: inherit;">{{ tag.name }}</a>
|
|
146 |
+ |
<a href="/discover?tag={{ tag.slug }}" class="tag">{{ tag.name }}</a>
|
| 201 |
147 |
|
{% endfor %}
|
| 202 |
148 |
|
</div>
|
| 203 |
149 |
|
<p class="item-description">{{ item.description }}</p>
|
| 252 |
198 |
|
{% else if session_user.is_some() %}
|
| 253 |
199 |
|
<form method="post" action="/stripe/subscribe/{{ tier.id }}">
|
| 254 |
200 |
|
<input type="hidden" name="_csrf" value="{{ csrf_token.as_deref().unwrap_or_default() }}">
|
| 255 |
|
- |
<details style="font-size: 0.85rem; margin-bottom: 0.5rem;">
|
| 256 |
|
- |
<summary style="cursor: pointer; opacity: 0.7;">Have a promo code?</summary>
|
| 257 |
|
- |
<input type="text" name="promo_code" placeholder="e.g. TRIAL14"
|
| 258 |
|
- |
style="width: 100%; margin-top: 0.4rem; padding: 0.3rem 0.5rem; font-size: 0.85rem; text-transform: uppercase;">
|
|
201 |
+ |
<details class="promo-details">
|
|
202 |
+ |
<summary>Have a promo code?</summary>
|
|
203 |
+ |
<input type="text" name="promo_code" placeholder="e.g. TRIAL14" class="promo-input">
|
| 259 |
204 |
|
</details>
|
| 260 |
|
- |
<button class="primary" type="submit">Subscribe</button>
|
|
205 |
+ |
<button class="primary" type="submit" data-loading-text="Redirecting to Stripe…">Subscribe</button>
|
| 261 |
206 |
|
</form>
|
| 262 |
207 |
|
{% else %}
|
| 263 |
|
- |
<a href="/login"><button class="primary" style="width: 100%;">Log in to Subscribe</button></a>
|
|
208 |
+ |
<a href="/login"><button class="primary login-cta-btn">Log in to Subscribe</button></a>
|
| 264 |
209 |
|
{% endif %}
|
| 265 |
210 |
|
</div>
|
| 266 |
211 |
|
{% endfor %}
|
| 270 |
215 |
|
|
| 271 |
216 |
|
<footer class="store-footer">
|
| 272 |
217 |
|
<p>Powered by <a href="/">Makenot<span class="dot">.</span>work</a> · Fair distribution for creatives of all kinds</p>
|
| 273 |
|
- |
<p style="margin-top: 0.5rem;">
|
|
218 |
+ |
<p class="store-footer-links">
|
| 274 |
219 |
|
<a href="javascript:void(0)" onclick="navigator.clipboard.writeText(window.location.origin + '/p/{{ project.slug }}').then(() => { this.textContent = 'Copied!'; setTimeout(() => this.textContent = 'Copy link', 1500) })">Copy link</a> ·
|
| 275 |
220 |
|
<a href="/policy">Policy</a> ·
|
| 276 |
221 |
|
{% if session_user.is_some() %}
|
| 277 |
|
- |
<a href="javascript:void(0)" onclick="document.getElementById('report-modal').style.display='flex'">Report</a>
|
|
222 |
+ |
<a href="javascript:void(0)" onclick="document.getElementById('report-modal').classList.remove('hidden')">Report</a>
|
| 278 |
223 |
|
{% else %}
|
| 279 |
224 |
|
<a href="/login">Report</a>
|
| 280 |
225 |
|
{% endif %}
|
| 300 |
245 |
|
container.className = 'items-container items-' + view + '-view';
|
| 301 |
246 |
|
}
|
| 302 |
247 |
|
document.querySelectorAll('.view-btn').forEach(function(btn) {
|
| 303 |
|
- |
btn.classList.toggle('active', btn.dataset.view === view);
|
|
248 |
+ |
btn.classList.toggle('is-selected', btn.dataset.view === view);
|
| 304 |
249 |
|
});
|
| 305 |
250 |
|
}
|
| 306 |
251 |
|
|