Skip to main content

max / makenotwork

12.9 KB · 276 lines History Blame Raw
1 {% extends "base.html" %}
2 {%- import "partials/carousel.html" as carousel -%}
3
4 {% block title %}{{ project.title }} - {{ creator_username }}{% endblock %}
5 {% block body_attrs %} class="padded-page project-page"{% endblock %}
6
7 {% block head %}
8 <meta property="og:title" content="{{ project.title }} by {{ creator_username }}">
9 <meta property="og:description" content="{{ project.description }}">
10 <meta property="og:type" content="website">
11 <meta property="og:url" content="{{ host_url }}/p/{{ project.slug }}">
12 <link rel="canonical" href="{{ host_url }}/p/{{ project.slug }}">
13 <meta property="og:site_name" content="Makenot.work">
14 {% if project.cover_image_url.is_some() %}<meta name="twitter:card" content="summary_large_image">{% else %}<meta name="twitter:card" content="summary">{% endif %}
15 <meta name="twitter:title" content="{{ project.title }} by {{ creator_username }}">
16 <meta name="twitter:description" content="{{ project.description }}">
17 {% if let Some(img) = project.cover_image_url %}
18 <meta property="og:image" content="{{ img }}">
19 <meta name="twitter:image" content="{{ img }}">
20 {% else %}
21 <meta property="og:image" content="{{ host_url }}/static/images/og-card.png">
22 <meta name="twitter:image" content="{{ host_url }}/static/images/og-card.png">
23 {% endif %}
24 <script type="application/ld+json">
25 {
26 "@context": "https://schema.org",
27 "@type": "CollectionPage",
28 "name": "{{ project.title_json()|safe }}",
29 "description": "{{ project.description_json()|safe }}",
30 "url": "{{ host_url }}/p/{{ project.slug }}",
31 "author": {
32 "@type": "Person",
33 "name": "{{ creator_username }}",
34 "url": "{{ host_url }}/u/{{ creator_username }}"
35 },
36 "numberOfItems": {{ project.item_count }},
37 "isPartOf": {
38 "@type": "WebSite",
39 "name": "Makenot.work",
40 "url": "{{ host_url }}"
41 }
42 }
43 </script>
44 <link rel="alternate" type="application/rss+xml" title="{{ project.title }} - RSS Feed" href="/p/{{ project.slug }}/rss">
45 {% endblock %}
46
47 {% block content %}
48 {% include "partials/site_header.html" %}
49
50 <div class="container">
51 <header class="store-header">
52 {% if let Some(img_url) = project.cover_image_url %}
53 <img src="{{ img_url }}" alt="{{ project.title }}" class="store-cover">
54 {% endif %}
55 <h1 class="store-title">{{ project.title }}<span class="dot">.</span></h1>
56 <div class="store-meta">
57 by <a href="/u/{{ creator_username }}">{{ creator_username }}</a> &middot;
58 <span>{{ project.item_count }} items</span>
59 </div>
60 <p class="store-description">{{ project.description }}</p>
61 <div class="store-actions">
62 {% if is_owner %}
63 <a href="/dashboard/project/{{ project.slug }}" class="btn-secondary">Edit Project</a>
64 {% endif %}
65 {% if session_user.is_some() %}
66 {% if is_following %}
67 <button class="btn-secondary follow-btn is-selected"
68 hx-delete="/api/follow/project/{{ project_id }}"
69 hx-swap="outerHTML">Following ({{ follower_count }})</button>
70 {% else %}
71 <button class="btn-secondary follow-btn"
72 hx-post="/api/follow/project/{{ project_id }}"
73 hx-swap="outerHTML">Follow ({{ follower_count }})</button>
74 {% endif %}
75 {% else if follower_count > 0 %}
76 <span class="follower-count-muted">{{ follower_count }} followers</span>
77 {% endif %}
78 <a href="/p/{{ project.slug }}/rss" class="btn-secondary">RSS Feed</a>
79 {% if has_blog_posts %}
80 <a href="/p/{{ project.slug }}/blog" class="btn-secondary">Blog</a>
81 {% endif %}
82 {% for repo in &git_repos %}
83 <a href="{{ repo.1 }}" class="btn-secondary">Git ({{ repo.0 }})</a>
84 {% endfor %}
85 {% if let Some(url) = community_url %}
86 <a href="{{ url }}" class="btn-secondary">Community</a>
87 {% endif %}
88 {% include "partials/tip_button.html" %}
89 </div>
90 </header>
91
92 {% if !gallery.is_empty() %}
93 <section class="project-gallery">
94 {% call carousel::carousel("project-gallery", gallery) %}
95 </section>
96 {% endif %}
97
98 {% if !sections.is_empty() %}
99 <section class="project-sections">
100 <div class="section-tabs">
101 {% for section in sections %}
102 <button class="section-tab{% if loop.first %} is-selected{% endif %}"
103 data-tab="section-{{ section.slug }}"
104 onclick="switchSectionTab(this, 'section-{{ section.slug }}')">{{ section.title }}</button>
105 {% endfor %}
106 </div>
107 {% for section in sections %}
108 <div class="section-panel{% if loop.first %} active{% endif %}" id="section-{{ section.slug }}">
109 {{ section.body_html|safe }}
110 </div>
111 {% endfor %}
112 </section>
113 <script>
114 function switchSectionTab(btn, panelId) {
115 // Tab selection uses .is-selected (charter); panel visibility
116 // still uses .active (display: none/block toggle).
117 document.querySelectorAll('.project-sections .section-tab').forEach(function(t) { t.classList.remove('is-selected'); });
118 document.querySelectorAll('.project-sections .section-panel').forEach(function(p) { p.classList.remove('active'); });
119 btn.classList.add('is-selected');
120 var panel = document.getElementById(panelId);
121 if (panel) panel.classList.add('active');
122 history.replaceState(null, '', '#' + panelId);
123 }
124 (function() {
125 var hash = window.location.hash.replace('#', '');
126 if (hash) {
127 var panel = document.getElementById(hash);
128 var tab = document.querySelector('.project-sections [data-tab="' + hash + '"]');
129 if (panel && tab) switchSectionTab(tab, hash);
130 }
131 })();
132 </script>
133 {% endif %}
134
135 <section class="items-section">
136 <div class="items-header">
137 <h2 class="section-header">Available Items</h2>
138 <div class="view-controls">
139 <button type="button" class="view-btn" data-view="list" title="List view">|||</button>
140 <button type="button" class="view-btn is-selected" data-view="grid" title="Grid view">:::</button>
141 </div>
142 </div>
143 <div class="items-container items-grid-view" id="items-container">
144 <div class="items-grid">
145 {% for item in items %}
146 <div class="item-card">
147 <a href="{% if item.can_access %}/i/{{ item.id }}{% else %}/purchase/{{ item.id }}{% endif %}" class="item-thumbnail">{{ item.thumbnail }}</a>
148 <div class="item-content">
149 <h3 class="item-title"><a href="{% if item.can_access %}/i/{{ item.id }}{% else %}/purchase/{{ item.id }}{% endif %}">{{ item.title }}</a></h3>
150 <div class="item-meta">{{ item.item_type }}{% if item.bundle_item_count > 0 %} ({{ item.bundle_item_count }} items){% endif %} &middot; {{ item.release_date }}</div>
151 <div class="item-tags">
152 {% for tag in item.tags %}
153 <a href="/discover?tag={{ tag.slug }}" class="tag">{{ tag.name }}</a>
154 {% endfor %}
155 </div>
156 <p class="item-description">{{ item.description }}</p>
157 <div class="item-footer">
158 <div class="item-price">{{ item.price }}</div>
159 <div class="item-stats">{{ item.sales_count }} sales</div>
160 </div>
161 {% if item.can_access %}
162 <a href="/l/{{ item.id }}" class="btn-secondary">View in library</a>
163 {% else if item.is_free %}
164 <button class="btn-primary"
165 hx-post="/api/library/add/{{ item.id }}"
166 hx-swap="outerHTML">Add to Library</button>
167 {% else if item.pwyw_enabled %}
168 <a href="/purchase/{{ item.id }}" class="btn-primary">Pay What You Want</a>
169 {% else %}
170 <a href="/purchase/{{ item.id }}" class="btn-primary">Buy Once</a>
171 {% endif %}
172 </div>
173 </div>
174 {% endfor %}
175 </div>
176
177 <!-- List View -->
178 <div class="items-list">
179 {% for item in items %}
180 <a href="{% if item.can_access %}/i/{{ item.id }}{% else %}/purchase/{{ item.id }}{% endif %}" class="list-item">
181 <div class="list-item-info">
182 <span class="list-item-title">{{ item.title }}</span>
183 <span class="list-item-meta">{{ item.item_type }}</span>
184 </div>
185 <div class="list-item-price">{{ item.price }}</div>
186 </a>
187 {% endfor %}
188 </div>
189 </div>
190 </section>
191
192 {% if !subscription_tiers.is_empty() %}
193 <section class="tiers-section">
194 <h2 class="section-header">Membership</h2>
195 <div class="tiers-grid">
196 {% for tier in subscription_tiers %}
197 <div class="tier-card">
198 <div class="tier-name">{{ tier.name }}</div>
199 <div class="tier-price">{{ tier.price }}</div>
200 {% if !tier.description.is_empty() %}
201 <p class="tier-description">{{ tier.description }}</p>
202 {% endif %}
203 {% if has_subscription %}
204 <div class="tier-active-badge">Subscribed</div>
205 {% else if session_user.is_some() %}
206 <form method="post" action="/stripe/subscribe/{{ tier.id }}">
207 <input type="hidden" name="_csrf" value="{{ csrf_token.as_deref().unwrap_or_default() }}">
208 <details class="promo-details">
209 <summary>Have a promo code?</summary>
210 <input type="text" name="promo_code" placeholder="e.g. TRIAL14" class="promo-input">
211 </details>
212 <button class="btn-primary" type="submit" data-loading-text="Redirecting to Stripe...">Subscribe</button>
213 </form>
214 {% else %}
215 <a href="/login" class="btn-primary">Log in to Subscribe</a>
216 {% endif %}
217 </div>
218 {% endfor %}
219 </div>
220 </section>
221 {% endif %}
222
223 <footer class="store-footer">
224 <p>Powered by <a href="/">Makenot<span class="dot">.</span>work</a> &middot; Fair distribution for creatives of all kinds</p>
225 <p class="store-footer-links">
226 <a href="/p/{{ project.slug }}" data-copy-link>Copy link</a> &middot;
227 <a href="/policy">Policy</a> &middot;
228 {% if session_user.is_some() %}
229 <a href="javascript:void(0)" onclick="document.getElementById('report-modal').classList.remove('hidden')">Report</a>
230 {% else %}
231 <a href="/login">Report</a>
232 {% endif %}
233 </p>
234 </footer>
235 </div>
236
237 {% if session_user.is_some() %}
238 {% let report_target_type = "project" %}
239 {% let report_target_id = project_id %}
240 {% let report_has_labels = true %}
241 {% include "partials/report_modal.html" %}
242 {% endif %}
243 {% endblock %}
244
245 {% block scripts %}
246 <script>
247 // View toggle with localStorage persistence
248 (function() {
249 function applyView(view) {
250 var container = document.getElementById('items-container');
251 if (container) {
252 container.className = 'items-container items-' + view + '-view';
253 }
254 document.querySelectorAll('.view-btn').forEach(function(btn) {
255 btn.classList.toggle('is-selected', btn.dataset.view === view);
256 });
257 }
258
259 // Load saved preference on page load
260 document.addEventListener('DOMContentLoaded', function() {
261 var saved = safeStorageGet('projectViewPref') || 'grid';
262 applyView(saved);
263 });
264
265 // Handle view button clicks
266 document.querySelectorAll('.view-btn').forEach(function(btn) {
267 btn.addEventListener('click', function() {
268 var view = btn.dataset.view;
269 applyView(view);
270 safeStorageSet('projectViewPref', view);
271 });
272 });
273 })();
274 </script>
275 {% endblock %}
276