Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_listing_details.html
Diff

/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/TEMPLATES/webstls_listing_details.html

added on local at 2026-07-01 13:48:04

Added
+638
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 123189026aaf
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<!-- =====================================================================
2 Public model detail page (buyer-facing). Two-column layout:
3 Left — hero image + thumbnail carousel + tabbed body content
4 (Description, Files, Print Settings, Reviews)
5 Right — sticky purchase card (designer, rating, price, actions,
6 meta rows)
7
8 dp- prefix on all classes follows the design reference at
9 html/Website/details.html. Markup is digital-only for now; physical
10 bits (Buy a Print action, Shipping tab) can be added later without
11 touching the surrounding shell.
12===================================================================== -->
13
14<!-- Inject the storefront's theme CSS variables before our own styles
15 so this listing page picks up the same --col-* values that the
16 storefront homepage / catalog pages use. Without this the page
17 falls back to site.css defaults and reads as a different site. -->
18<style>:root { $theme_css_vars }</style>
19
20<style>
21 /*
22 THEME-AWARE — every color here comes from the storefront's active
23 theme via the --col-* CSS variables that MODS::AffSoft::Themes.pm
24 injects into every store.cgi page. This means the same listing
25 details template renders correctly across all 18+ store layouts
26 AND all color themes (dark, light, neon, paper, etc.) without
27 duplicating the markup per theme. Only semantic colors that
28 transcend theme (green for "Free", red for danger, gold for
29 stars) stay hardcoded -- those aren't supposed to change between
30 themes anyway.
31 */
32
33 /* Breadcrumb */
34 .dp-breadcrumb { background:var(--col-surface-1);border-bottom:1px solid var(--col-border);padding:14px 32px; }
35 .dp-breadcrumb-inner { max-width:1400px;margin:0 auto;display:flex;align-items:center;gap:8px;font-size:13px;color:var(--col-text-3); }
36 .dp-breadcrumb a { color:var(--col-text-2);text-decoration:none;transition:color 0.15s; }
37 .dp-breadcrumb a:hover { color:var(--col-accent-bright); }
38 .dp-breadcrumb-sep { font-size:11px;color:var(--col-text-3);opacity:0.6; }
39 .dp-breadcrumb-current { color:var(--col-text);font-weight:600; }
40
41 /* Layout */
42 .dp-layout { max-width:1400px;margin:0 auto;padding:28px 32px 80px;display:grid;grid-template-columns:1fr 400px;gap:36px;align-items:start; }
43 .dp-left { display:flex;flex-direction:column;gap:18px;min-width:0; }
44 .dp-right { display:flex;flex-direction:column;gap:16px; }
45
46 /* Hero */
47 .dp-hero-img { position:relative;border-radius:16px;overflow:hidden;border:1px solid var(--col-border);aspect-ratio:16/10;background:var(--col-surface-2); }
48 .dp-hero-img img { width:100%;height:100%;object-fit:cover;display:block;transition:transform 0.4s ease; }
49 .dp-hero-img:hover img { transform:scale(1.02); }
50 .dp-hero-overlay { position:absolute;inset:0;background:linear-gradient(to top, var(--col-bg) 0%, rgba(0,0,0,0.05) 45%, transparent 100%);opacity:0.85;pointer-events:none; }
51 .dp-hero-title-wrap { position:absolute;bottom:0;left:0;right:0;padding:24px 28px; }
52 .dp-hero-eyebrow { font-size:11px;font-weight:700;color:var(--col-accent-bright);letter-spacing:2px;text-transform:uppercase;margin-bottom:6px; }
53 .dp-hero-title { font-family:var(--font-display,sans-serif);font-size:clamp(28px,4vw,46px);color:var(--col-text);line-height:1.0;letter-spacing:0.5px; }
54 .dp-hero-badges { display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap; }
55 .dp-hero-badge { padding:3px 10px;border-radius:6px;font-size:11px;font-weight:700; }
56 .dp-badge-cat { background:rgba(0,0,0,0.35);border:1px solid var(--col-border);color:#fff;font-weight:700;border-radius:6px; }
57 .dp-badge-src { background:rgba(0,0,0,0.35);border:1px solid var(--col-border);color:#fff;font-weight:700;border-radius:6px; }
58 .dp-badge-free { background:rgba(34,197,94,0.18);border:1px solid rgba(34,197,94,0.4);color:#4ade80; }
59
60 /* Thumbs */
61 .dp-thumbs { display:flex;gap:8px;overflow-x:auto;padding-bottom:4px; }
62 .dp-thumb { flex-shrink:0;width:120px;height:84px;border-radius:8px;overflow:hidden;border:2px solid var(--col-border);cursor:pointer;transition:border-color 0.2s,box-shadow 0.2s;background:var(--col-surface-2);position:relative; }
63 .dp-thumb:hover { border-color:var(--col-accent); }
64 .dp-thumb.active { border-color:var(--col-accent-bright);box-shadow:0 0 0 1px var(--col-accent-bright); }
65 .dp-thumb img { width:100%;height:100%;object-fit:cover;display:block; }
66 .dp-thumb-num { position:absolute;top:3px;left:3px;background:var(--col-bg);color:var(--col-text);font-size:10px;font-weight:800;padding:2px 7px;border-radius:5px;letter-spacing:0.5px;border:1px solid var(--col-border-2);opacity:0.92; }
67
68 /* Tabs */
69 .dp-tabs { background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:14px;overflow:hidden; }
70 .dp-tab-nav { display:flex;border-bottom:1px solid var(--col-border);background:var(--col-surface-2); }
71 .dp-tab-btn { flex:1;padding:13px 14px;background:none;border:none;border-bottom:2px solid transparent;font-size:13px;font-weight:700;color:var(--col-text-2);cursor:pointer;transition:all 0.2s;letter-spacing:0.3px;font-family:inherit; }
72 .dp-tab-btn:hover { color:var(--col-text);background:var(--col-surface-1); }
73 .dp-tab-btn.active { color:#fff;border-bottom-color:var(--col-accent);background:var(--col-accent-glow);font-weight:800; }
74 .dp-tab-pane { padding:24px 28px;display:none; }
75 .dp-tab-pane.active { display:block; }
76 .dp-tab-empty { color:var(--col-text-3);font-size:13px;font-style:italic;text-align:center;padding:30px 0; }
77
78 /* Description tab */
79 .dp-desc { font-size:14.5px;color:var(--col-text-2);line-height:1.75; }
80 .dp-desc p { margin:0 0 14px; }
81 .dp-desc p:last-child { margin-bottom:0; }
82 .dp-desc-h { font-size:12px;font-weight:700;color:var(--col-accent-bright);letter-spacing:1.5px;text-transform:uppercase;margin:24px 0 12px; }
83 .dp-highlights { display:grid;grid-template-columns:1fr 1fr;gap:10px; }
84 .dp-h-item { display:flex;align-items:flex-start;gap:10px;padding:12px 14px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px; }
85 .dp-h-dot { width:6px;height:6px;border-radius:50%;background:var(--col-accent);margin-top:6px;flex-shrink:0;box-shadow:0 0 6px var(--col-accent-glow); }
86 .dp-h-text { font-size:13px;color:var(--col-text-2);line-height:1.5; }
87 .dp-included { display:flex;flex-direction:column;gap:8px; }
88 .dp-inc-item { display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:9px;font-size:13px;color:var(--col-text-2); }
89 .dp-inc-dot { width:7px;height:7px;border-radius:50%;background:var(--col-accent-bright);box-shadow:0 0 6px var(--col-accent-glow);flex-shrink:0; }
90
91 /* Files table */
92 .dp-files-table { width:100%;border-collapse:collapse; }
93 .dp-files-table th { text-align:left;padding:10px 14px;font-size:11px;font-weight:700;color:var(--col-text-3);letter-spacing:1px;text-transform:uppercase;border-bottom:1px solid var(--col-border); }
94 .dp-files-table td { padding:12px 14px;font-size:13px;color:var(--col-text-2);border-bottom:1px solid var(--col-border);opacity:0.95; }
95 .dp-files-table tr:last-child td { border-bottom:none; }
96 .dp-files-table tr:hover td { background:var(--col-surface-2);color:var(--col-text); }
97 .dp-file-name { color:var(--col-text);font-weight:600; }
98 /* File-format chips keep semantic colors (each ext gets its own
99 hue) -- these read as filetypes regardless of theme. */
100 .dp-file-fmt { display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:700;font-family:var(--font-mono,monospace); }
101 .dp-fmt-stl { background:rgba(34,197,94,0.10);border:1px solid rgba(34,197,94,0.28);color:#4ade80; }
102 .dp-fmt-3mf { background:rgba(99,102,241,0.10);border:1px solid rgba(99,102,241,0.28);color:#a5b4fc; }
103 .dp-fmt-step { background:rgba(6,182,212,0.10);border:1px solid rgba(6,182,212,0.28);color:#22d3ee; }
104 .dp-fmt-obj { background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.28);color:#fbbf24; }
105 .dp-fmt-zip { background:rgba(124,58,237,0.10);border:1px solid rgba(124,58,237,0.28);color:#c4b5fd; }
106 .dp-fmt-other { background:rgba(148,163,184,0.10);border:1px solid rgba(148,163,184,0.28);color:#cbd5e1; }
107
108 /* Print settings */
109 .dp-settings-grid { display:grid;grid-template-columns:1fr 1fr;gap:14px; }
110 .dp-setting-card { padding:16px 18px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:12px; }
111 .dp-setting-label { font-size:11px;font-weight:700;color:var(--col-text-3);letter-spacing:1px;text-transform:uppercase;margin-bottom:6px; }
112 .dp-setting-value { font-family:var(--font-mono,monospace);font-size:15px;font-weight:600;color:var(--col-accent-bright); }
113
114 /* Reviews */
115 .dp-rv-summary { display:flex;align-items:center;gap:24px;padding:18px 20px;margin-bottom:16px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:12px; }
116 .dp-rv-big { font-family:var(--font-display,sans-serif);font-size:48px;color:var(--col-text);line-height:1; }
117 /* Gold stars stay semantic — read as rating regardless of theme. */
118 .dp-rv-stars-big { color:#f59e0b;font-size:16px;letter-spacing:2px; }
119 .dp-rv-total { font-size:12px;color:var(--col-text-3);margin-top:4px; }
120 .dp-rv-empty { padding:34px 18px;text-align:center;color:var(--col-text-3);font-size:13px; }
121
122 /* Purchase card */
123 .dp-pc { position:sticky;top:24px;background:var(--col-surface-1);border:1px solid var(--col-border);border-radius:16px;overflow:hidden; }
124 .dp-pc-inner { padding:24px 26px; }
125 .dp-pc-title { font-family:var(--font-display,sans-serif);font-size:26px;color:var(--col-text);letter-spacing:0.3px;line-height:1.1;margin-bottom:14px; }
126
127 .dp-designer-row { display:flex;align-items:center;gap:12px;margin-bottom:18px;padding-bottom:18px;border-bottom:1px solid var(--col-border); }
128 .dp-designer-avatar { width:42px;height:42px;border-radius:50%;background:linear-gradient(135deg,var(--col-accent),var(--col-accent-deep));display:flex;align-items:center;justify-content:center;font-family:var(--font-display,sans-serif);font-size:17px;color:#fff;flex-shrink:0;border:2px solid var(--col-accent); }
129 .dp-designer-info { flex:1;min-width:0; }
130 .dp-designer-by { font-size:10px;color:var(--col-text-3);text-transform:uppercase;letter-spacing:0.8px; }
131 .dp-designer-name { font-size:14px;font-weight:700;color:var(--col-accent-bright);text-decoration:none; }
132 .dp-designer-name:hover { color:var(--col-text); }
133 .dp-store-link { color:var(--col-text-2);text-decoration:none;font-size:12px; }
134 .dp-store-link:hover { color:var(--col-text); }
135
136 .dp-rating-block { display:flex;align-items:center;gap:10px;margin-bottom:18px; }
137 .dp-rating-stars { color:#f59e0b;font-size:15px;letter-spacing:1px; }
138 .dp-rating-num { font-size:15px;font-weight:700;color:var(--col-text); }
139 .dp-rating-count { font-size:12px;color:var(--col-text-3); }
140
141 .dp-price-block { display:flex;align-items:flex-end;gap:12px;margin-bottom:18px; }
142 .dp-price-main { font-family:var(--font-display,sans-serif);font-size:42px;color:var(--col-text);line-height:1; }
143 .dp-price-free { padding:4px 10px;border-radius:6px;background:rgba(34,197,94,0.16);border:1px solid rgba(34,197,94,0.4);color:#4ade80;font-size:12px;font-weight:700;margin-bottom:6px; }
144
145 .dp-actions { display:flex;flex-direction:column;gap:10px;margin-bottom:18px; }
146 .dp-btn-primary { width:100%;padding:14px 0;background:linear-gradient(135deg,var(--col-accent),var(--col-accent-deep));color:#fff;border:none;border-radius:11px;font-size:14.5px;font-weight:700;cursor:pointer;transition:transform 0.15s,box-shadow 0.15s;box-shadow:0 8px 24px var(--col-accent-glow);letter-spacing:0.3px;font-family:inherit; }
147 .dp-btn-primary:hover { transform:translateY(-2px);filter:brightness(1.08); }
148 .dp-btn-secondary { width:100%;padding:12px 0;background:var(--col-surface-2);border:1px solid var(--col-border-2);border-radius:11px;color:var(--col-text);font-size:13.5px;font-weight:600;cursor:pointer;transition:all 0.15s;font-family:inherit; }
149 .dp-btn-secondary:hover { background:var(--col-accent-glow);border-color:var(--col-accent); }
150 .dp-btn-wishlist { width:100%;padding:10px 0;background:none;border:1px solid var(--col-border);border-radius:11px;color:var(--col-text-3);font-size:13px;font-weight:600;cursor:pointer;transition:all 0.15s;font-family:inherit; }
151 .dp-btn-wishlist:hover { border-color:rgba(239,68,68,0.4);color:#f87171; }
152
153 /* Meta rows */
154 .dp-meta { display:flex;flex-direction:column;gap:1px;background:var(--col-border);border-radius:10px;overflow:hidden;margin-top:14px; }
155 .dp-meta-row { display:flex;justify-content:space-between;align-items:center;padding:11px 14px;background:var(--col-surface-2);font-size:12.5px; }
156 .dp-meta-label { color:var(--col-text-3);font-weight:500; }
157 .dp-meta-value { color:var(--col-text);font-weight:600; }
158 .dp-meta-value.blue { color:var(--col-accent-bright); }
159 .dp-meta-value.green { color:#4ade80; }
160
161 /* Tag chips */
162 .dp-tags { display:flex;flex-wrap:wrap;gap:6px;margin-top:14px; }
163 .dp-tag { display:inline-block;padding:5px 11px;background:rgba(0,0,0,0.35);border:1px solid var(--col-border);border-radius:6px;font-size:11.5px;font-weight:600;color:#fff;text-decoration:none;letter-spacing:0.3px; }
164 .dp-tag:hover { background:var(--col-accent);color:#fff;border-color:var(--col-accent); }
165
166 /* Source-platform note */
167 .dp-source-note { margin-top:14px;padding:11px 14px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px;font-size:12px;color:var(--col-text-2); }
168 .dp-source-note a { color:var(--col-accent-bright);text-decoration:underline; }
169
170 /* License footer */
171 .dp-license { margin:0 20px 20px;padding:14px 16px;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px;font-size:11.5px;color:var(--col-text-3);line-height:1.55; }
172
173 /* Related strip */
174 .dp-section { padding:48px 32px;background:var(--col-surface-1);border-top:1px solid var(--col-border); }
175 .dp-section-inner { max-width:1400px;margin:0 auto; }
176 .dp-section-hd { display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:22px; }
177 .dp-section-eyebrow { font-size:11px;font-weight:700;color:var(--col-accent-bright);letter-spacing:2px;text-transform:uppercase;margin-bottom:4px; }
178 .dp-section-title { font-family:var(--font-display,sans-serif);font-size:26px;color:var(--col-text);letter-spacing:0.3px;line-height:1; }
179 .dp-rel-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px; }
180 .dp-rel-card { background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:12px;overflow:hidden;text-decoration:none;display:block;transition:transform 0.2s,border-color 0.2s; }
181 .dp-rel-card:hover { transform:translateY(-4px);border-color:var(--col-accent); }
182 .dp-rel-thumb { position:relative;aspect-ratio:4/3;overflow:hidden;background:var(--col-surface-1); }
183 .dp-rel-thumb img { width:100%;height:100%;object-fit:cover; }
184 .dp-rel-price { position:absolute;top:8px;right:8px;padding:2px 9px;border-radius:14px;font-size:11px;font-weight:700;background:var(--col-surface-1);border:1px solid var(--col-border-2);color:var(--col-text); }
185 .dp-rel-price.free { background:rgba(34,197,94,0.2);border-color:rgba(34,197,94,0.4);color:#4ade80; }
186 .dp-rel-body { padding:10px 12px 12px; }
187 .dp-rel-title { font-size:13px;font-weight:700;color:var(--col-text);margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
188 .dp-rel-foot { display:flex;align-items:center;gap:7px;font-size:11px;color:var(--col-text-3); }
189 .dp-rel-rating { color:#f59e0b;font-weight:700; }
190
191 /* Responsive */
192 @media (max-width:1024px) {
193 .dp-layout { grid-template-columns:1fr;padding:20px 22px 60px; }
194 .dp-pc { position:static; }
195 .dp-highlights { grid-template-columns:1fr; }
196 .dp-settings-grid { grid-template-columns:1fr; }
197 }
198</style>
199
200
201<!-- The storefront header (brand + search + sign in + cart + nav)
202 is now injected by listing_details.cgi via the shared
203 MODS::AffSoft::StoreChrome helper, so every buyer-facing page
204 (cart, checkout, listing_details, etc.) shows the same chrome
205 and there is no duplicate header here. -->
206
207
208<!-- ===== Breadcrumb ============================================== -->
209<nav class="dp-breadcrumb" aria-label="Breadcrumb">
210 <div class="dp-breadcrumb-inner">
211 <a href="/store.cgi?subdomain=$store_subdomain">$store_name</a>
212 <span class="dp-breadcrumb-sep">&rsaquo;</span>
213 <a href="/store.cgi?subdomain=$store_subdomain&page=shop">$breadcrumb_cat</a>
214 <span class="dp-breadcrumb-sep">&rsaquo;</span>
215 <span class="dp-breadcrumb-current">$title</span>
216 </div>
217</nav>
218
219
220<!-- ===== Main two-column ======================================== -->
221<div class="dp-layout">
222
223 <!-- ─── LEFT ─────────────────────────────────────────────────── -->
224 <div class="dp-left">
225
226 <!-- Hero image -->
227 <div class="dp-hero-img">
228 <img src="$hero_url" alt="$title" id="dpHero">
229 <div class="dp-hero-overlay"></div>
230 <div class="dp-hero-title-wrap">
231 [if:$has_category]<div class="dp-hero-eyebrow">$category</div>[/if]
232 <div class="dp-hero-title">$title</div>
233 <div class="dp-hero-badges">
234 [if:$has_category]<span class="dp-hero-badge dp-badge-cat">$category</span>[/if]
235 [if:$is_free]<span class="dp-hero-badge dp-badge-free">Free download</span>[/if]
236 [if:$has_source]<span class="dp-hero-badge dp-badge-src">From $source_label</span>[/if]
237 </div>
238 </div>
239 </div>
240
241 <!-- Thumbnail strip -->
242 [if:$has_gallery]
243 <div class="dp-thumbs">
244 [loop:@gallery]
245 <div class="dp-thumb[if:$loop1.is_active] active[/if]" data-dp-thumb="$loop1.url">
246 <span class="dp-thumb-num">$loop1.idx</span>
247 <img src="$loop1.url" alt="$loop1.alt" loading="lazy">
248 </div>
249 [/loop]
250 </div>
251 [/if]
252
253 <!-- Tabs -->
254 <div class="dp-tabs">
255 <div class="dp-tab-nav" role="tablist">
256 <button class="dp-tab-btn active" data-dp-tab="dp-desc">Description</button>
257 [if:$has_files]<button class="dp-tab-btn" data-dp-tab="dp-files">Files Included</button>[/if]
258 [if:$has_print_settings]<button class="dp-tab-btn" data-dp-tab="dp-print">Print Settings</button>[/if]
259 <button class="dp-tab-btn" data-dp-tab="dp-reviews">Reviews</button>
260 </div>
261
262 <!-- DESCRIPTION -->
263 <div class="dp-tab-pane active" id="dp-desc">
264 [if:$has_tagline]<div style="font-size:16px;color:#e2e8f0;font-weight:600;margin-bottom:18px;line-height:1.5">$tagline</div>[/if]
265 [if:$has_description]<div class="dp-desc">$description</div>[/if]
266 [if:!$has_description][if:!$has_tagline]<div class="dp-tab-empty">The seller hasn&#39;t added a description yet.</div>[/if][/if]
267
268 [if:$has_highlights]
269 <div class="dp-desc-h">Highlights</div>
270 <div class="dp-highlights">
271 [loop:@highlights]
272 <div class="dp-h-item">
273 <span class="dp-h-dot"></span>
274 <span class="dp-h-text">$loop1.text</span>
275 </div>
276 [/loop]
277 </div>
278 [/if]
279
280 [if:$has_included]
281 <div class="dp-desc-h">What&#39;s in the Pack</div>
282 <div class="dp-included">
283 [loop:@included]
284 <div class="dp-inc-item">
285 <span class="dp-inc-dot"></span>
286 <span>$loop1.text</span>
287 </div>
288 [/loop]
289 </div>
290 [/if]
291 </div>
292
293 <!-- FILES -->
294 [if:$has_files]
295 <div class="dp-tab-pane" id="dp-files">
296 <table class="dp-files-table">
297 <thead>
298 <tr><th>File Name</th><th>Format</th><th>Size</th></tr>
299 </thead>
300 <tbody>
301 [loop:@files]
302 <tr>
303 <td class="dp-file-name">$loop1.name</td>
304 <td><span class="dp-file-fmt $loop1.fmt_class">$loop1.ext</span></td>
305 <td>$loop1.size_human</td>
306 </tr>
307 [/loop]
308 </tbody>
309 </table>
310 </div>
311 [/if]
312
313 <!-- PRINT SETTINGS -->
314 [if:$has_print_settings]
315 <div class="dp-tab-pane" id="dp-print">
316 <div class="dp-settings-grid">
317 [loop:@print_settings]
318 <div class="dp-setting-card">
319 <div class="dp-setting-label">$loop1.label</div>
320 <div class="dp-setting-value">$loop1.value</div>
321 </div>
322 [/loop]
323 </div>
324 </div>
325 [/if]
326
327 <!-- REVIEWS -->
328 <div class="dp-tab-pane" id="dp-reviews">
329 [if:$has_rating]
330 <div class="dp-rv-summary">
331 <div style="text-align:center">
332 <div class="dp-rv-big">$rating_num</div>
333 <div class="dp-rv-stars-big">$rating_stars</div>
334 <div class="dp-rv-total">$rating_n review(s)</div>
335 </div>
336 <div style="flex:1;color:#64748b;font-size:13px">Reviews from verified buyers appear here as soon as a sale is rated.</div>
337 </div>
338 [/if]
339 [if:!$has_rating]<div class="dp-rv-empty">No reviews yet. Be the first to review this design after your purchase.</div>[/if]
340 </div>
341
342 </div><!-- /.dp-tabs -->
343 </div><!-- /.dp-left -->
344
345
346 <!-- ─── RIGHT ─────────────────────────────────────────────────── -->
347 <div class="dp-right">
348 <div class="dp-pc">
349 <div class="dp-pc-inner">
350
351 <div class="dp-pc-title">$title</div>
352
353 <!-- Designer -->
354 <div class="dp-designer-row">
355 <div class="dp-designer-avatar">$designer_initial</div>
356 <div class="dp-designer-info">
357 <div class="dp-designer-by">Designed by</div>
358 <a href="/store.cgi?subdomain=$store_subdomain" class="dp-designer-name">$designer_name</a>
359 <div><a href="/store.cgi?subdomain=$store_subdomain" class="dp-store-link">$store_name</a></div>
360 </div>
361 </div>
362
363 <!-- Rating -->
364 [if:$has_rating]
365 <div class="dp-rating-block">
366 <span class="dp-rating-stars">$rating_stars</span>
367 <span class="dp-rating-num">$rating_num</span>
368 <span class="dp-rating-count">/ 5.0 &middot; $rating_n review(s)</span>
369 </div>
370 [/if]
371
372 <!-- Price + (when applicable) printed copy + variant chooser.
373 Three flavors:
374 digital -> single price + Add to Cart (legacy path)
375 physical -> printed price + color/material picker + stock
376 both -> kind toggle, swaps price + reveals variant
377 picker only when buyer chooses Printed copy -->
378 [if:$is_kind_digital]
379 <div class="dp-price-block">
380 <div class="dp-price-main">$price_label</div>
381 [if:$is_free]<div class="dp-price-free">FREE</div>[/if]
382 </div>
383 [/if]
384
385 [if:$is_kind_physical]
386 <div class="dp-price-block">
387 <div class="dp-price-main">$phys_price_label</div>
388 <div class="text-xs text-dim" style="margin-top:4px;line-height:1.5">
389 $prod_label[if:$has_ship_from] &middot; $ship_from_label[/if]<br>
390 <strong>$stock_label</strong>
391 </div>
392 </div>
393 [/if]
394
395 [if:$is_kind_both]
396 <div class="dp-price-block" id="dpKindBlock">
397 <div role="tablist" style="display:flex;gap:0;background:var(--col-surface-2);border:1px solid var(--col-border);border-radius:10px;padding:4px;margin-bottom:14px">
398 <button type="button" id="dpKindDigital" data-kind="digital" role="tab" style="flex:1;padding:10px 8px;background:var(--col-accent);color:#fff;border:0;border-radius:7px;font-family:inherit;font-size:12px;font-weight:700;letter-spacing:0.5px;text-transform:uppercase;cursor:pointer">Digital file</button>
399 <button type="button" id="dpKindPhysical" data-kind="physical" role="tab" style="flex:1;padding:10px 8px;background:transparent;color:var(--col-text-2);border:0;border-radius:7px;font-family:inherit;font-size:12px;font-weight:700;letter-spacing:0.5px;text-transform:uppercase;cursor:pointer">Printed copy</button>
400 </div>
401 <div class="dp-price-main" id="dpPriceLabel">$price_label</div>
402 <div class="text-xs text-dim" id="dpPriceMeta" style="margin-top:4px;line-height:1.5;display:none">
403 $prod_label[if:$has_ship_from] &middot; $ship_from_label[/if]<br>
404 <strong>$stock_label</strong>
405 </div>
406 </div>
407 [/if]
408
409 <!-- Color + Material picker. Shown when buyer is buying the
410 printed copy. JS toggles visibility based on chosen kind. -->
411 [if:$offers_physical]
412 <div id="dpVariantPicker" style="display:[if:$is_kind_physical]block[/if][if:!$is_kind_physical]none[/if];margin-bottom:14px">
413 [if:$has_color_choices]
414 <label class="form-label" style="font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--col-text-3);display:block;margin-bottom:4px">Color</label>
415 <select id="dpColor" class="select" style="width:100%;margin-bottom:10px">
416 [loop:@color_choices]
417 <option value="$loop1.value">$loop1.label</option>
418 [/loop]
419 </select>
420 [/if]
421 [if:$has_material_choices]
422 <label class="form-label" style="font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--col-text-3);display:block;margin-bottom:4px">Material</label>
423 <select id="dpMaterial" class="select" style="width:100%">
424 [loop:@material_choices]
425 <option value="$loop1.value">$loop1.label</option>
426 [/loop]
427 </select>
428 [/if]
429 </div>
430 [/if]
431
432 <!-- Actions -->
433 <div class="dp-actions">
434 <button type="button" class="dp-btn-primary we-add-to-cart" id="dpAddToCart" data-mid="$model_id" data-sid="$store_id" data-kind="$product_kind"[if:$is_sold_out] disabled[/if]>
435 [if:$is_sold_out]Sold out[/if][if:!$is_sold_out][if:$is_free]&darr; Download for Free[/if][if:!$is_free]&darr; Add to Cart[/if][/if]
436 </button>
437 [if:$has_files][if:!$is_kind_physical]<button type="button" class="dp-btn-secondary">&#9654; Preview Files</button>[/if][/if]
438 <button type="button" class="dp-btn-wishlist">&#9825; Add to Wishlist</button>
439 </div>
440
441 <!-- Meta rows -->
442 <div class="dp-meta">
443 [if:$has_files]
444 <div class="dp-meta-row">
445 <span class="dp-meta-label">Files included</span>
446 <span class="dp-meta-value">$file_count file(s)</span>
447 </div>
448 <div class="dp-meta-row">
449 <span class="dp-meta-label">Total size</span>
450 <span class="dp-meta-value">$total_size</span>
451 </div>
452 [/if]
453 <div class="dp-meta-row">
454 <span class="dp-meta-label">Total downloads</span>
455 <span class="dp-meta-value blue">&darr; $total_downloads</span>
456 </div>
457 [if:$has_category]
458 <div class="dp-meta-row">
459 <span class="dp-meta-label">Category</span>
460 <span class="dp-meta-value">$category</span>
461 </div>
462 [/if]
463 [if:$has_updated]
464 <div class="dp-meta-row">
465 <span class="dp-meta-label">Updated</span>
466 <span class="dp-meta-value">$updated_pretty</span>
467 </div>
468 [/if]
469 </div>
470
471 <!-- Tags -->
472 [if:$has_tags]
473 <div class="dp-tags">
474 [loop:@tags]
475 <span class="dp-tag">$loop1.name</span>
476 [/loop]
477 </div>
478 [/if]
479
480 <!-- Source platform note -->
481 [if:$has_source]
482 <div class="dp-source-note">
483 This design was originally posted on <strong>$source_label</strong>. <a href="$source_url" target="_blank" rel="noopener">View the original listing &rarr;</a>
484 </div>
485 [/if]
486
487 </div><!-- /.dp-pc-inner -->
488
489 <div class="dp-license">
490 Licensed as <strong style="color:#94a3b8">$license</strong>. Digital files are delivered after purchase; redistribution is not permitted.
491 </div>
492 </div><!-- /.dp-pc -->
493 </div><!-- /.dp-right -->
494
495</div><!-- /.dp-layout -->
496
497
498<!-- ===== More from this designer ================================ -->
499[if:$has_related]
500<section class="dp-section">
501 <div class="dp-section-inner">
502 <div class="dp-section-hd">
503 <div>
504 <div class="dp-section-eyebrow">More from $designer_name</div>
505 <div class="dp-section-title">Other designs from this seller</div>
506 </div>
507 <a href="/store.cgi?subdomain=$store_subdomain" style="font-size:13px;font-weight:600;color:#60a5fa;text-decoration:none;padding:8px 16px;border:1px solid rgba(59,130,246,0.3);border-radius:8px">View all &rarr;</a>
508 </div>
509 <div class="dp-rel-grid">
510 [loop:@related]
511 <a href="/listing_details.cgi?id=$loop1.id" class="dp-rel-card">
512 <div class="dp-rel-thumb">
513 <img src="$loop1.hero" alt="$loop1.title" loading="lazy">
514 <span class="dp-rel-price[if:$loop1.is_free] free[/if]">$loop1.price_label</span>
515 </div>
516 <div class="dp-rel-body">
517 <div class="dp-rel-title">$loop1.title</div>
518 [if:$loop1.rating]
519 <div class="dp-rel-foot">
520 <span class="dp-rel-rating">&#9733; $loop1.rating</span>
521 <span>&middot; $loop1.rating_n review(s)</span>
522 </div>
523 [/if]
524 </div>
525 </a>
526 [/loop]
527 </div>
528 </div>
529</section>
530[/if]
531
532
533<!-- ===== JS: thumbnail click + tab switch ======================== -->
534<script>
535(function(){
536 // Thumbnail click swaps the hero image. Active class moves with it.
537 var hero = document.getElementById('dpHero');
538 var thumbs = document.querySelectorAll('[data-dp-thumb]');
539 thumbs.forEach(function(t){
540 t.addEventListener('click', function(){
541 if (!hero) return;
542 var url = this.getAttribute('data-dp-thumb');
543 if (!url) return;
544 hero.src = url;
545 thumbs.forEach(function(o){ o.classList.remove('active'); });
546 this.classList.add('active');
547 });
548 });
549
550 // Tab switcher. Pure JS, no framework. Single .active on the
551 // currently visible tab pane / button pair.
552 var tabBtns = document.querySelectorAll('[data-dp-tab]');
553 tabBtns.forEach(function(btn){
554 btn.addEventListener('click', function(){
555 var target = this.getAttribute('data-dp-tab');
556 if (!target) return;
557 tabBtns.forEach(function(o){ o.classList.remove('active'); });
558 this.classList.add('active');
559 document.querySelectorAll('.dp-tab-pane').forEach(function(p){ p.classList.remove('active'); });
560 var pane = document.getElementById(target);
561 if (pane) pane.classList.add('active');
562 });
563 });
564
565 // Kind toggle for 'both' listings. Swaps the headline price, the
566 // ships-in / stock meta strip, the data-kind attribute on the Add
567 // to Cart button (which the POST below reads), and the visibility
568 // of the color/material picker.
569 var kDigital = document.getElementById('dpKindDigital');
570 var kPhysical = document.getElementById('dpKindPhysical');
571 var priceLabel = document.getElementById('dpPriceLabel');
572 var priceMeta = document.getElementById('dpPriceMeta');
573 var variantBox = document.getElementById('dpVariantPicker');
574 var addBtn = document.getElementById('dpAddToCart');
575 var digitalPriceText = "$price_label";
576 var physicalPriceText = "$phys_price_label";
577 if (kDigital && kPhysical && addBtn) {
578 function setKind(kind) {
579 var picked = (kind === 'physical');
580 kDigital.style.background = picked ? 'transparent' : 'var(--col-accent)';
581 kDigital.style.color = picked ? 'var(--col-text-2)' : '#fff';
582 kPhysical.style.background = picked ? 'var(--col-accent)' : 'transparent';
583 kPhysical.style.color = picked ? '#fff' : 'var(--col-text-2)';
584 if (priceLabel) priceLabel.textContent = picked ? physicalPriceText : digitalPriceText;
585 if (priceMeta) priceMeta.style.display = picked ? 'block' : 'none';
586 if (variantBox) variantBox.style.display = picked ? 'block' : 'none';
587 addBtn.setAttribute('data-kind', kind);
588 }
589 kDigital.addEventListener('click', function(){ setKind('digital'); });
590 kPhysical.addEventListener('click', function(){ setKind('physical'); });
591 }
592
593 // Add to cart. Reads model+storefront from the button, kind+color
594 // +material from the variant picker. POSTs to /cart_add.cgi and
595 // updates the header cart badge if present.
596 function getVal(id) { var el = document.getElementById(id); return el ? el.value : ''; }
597 if (addBtn) {
598 addBtn.addEventListener('click', function() {
599 if (addBtn.disabled) return;
600 var mid = addBtn.getAttribute('data-mid');
601 var sid = addBtn.getAttribute('data-sid');
602 var kind = addBtn.getAttribute('data-kind') || 'digital';
603 var color = (kind === 'physical') ? getVal('dpColor') : '';
604 var material = (kind === 'physical') ? getVal('dpMaterial') : '';
605 var orig = addBtn.textContent;
606 addBtn.disabled = true;
607 addBtn.textContent = 'Adding...';
608 var fd = new FormData();
609 fd.append('model_id', mid);
610 fd.append('storefront_id', sid);
611 fd.append('kind', kind);
612 fd.append('color', color);
613 fd.append('material', material);
614 fetch('/cart_add.cgi', { method: 'POST', body: fd, credentials: 'same-origin' })
615 .then(function(r) { return r.json(); })
616 .then(function(data) {
617 if (!data || !data.success) throw new Error(data && data.error || 'add failed');
618 addBtn.textContent = 'Added ✓';
619 var link = document.getElementById('we-cart-link');
620 if (link) {
621 var badge = link.querySelector('.we-cart-badge');
622 if (badge) { badge.textContent = data.count; }
623 }
624 setTimeout(function() { addBtn.disabled = false; addBtn.textContent = orig; }, 1500);
625 })
626 .catch(function(err) {
627 addBtn.disabled = false;
628 addBtn.textContent = orig;
629 if (typeof appToast === 'function') {
630 appToast({ message: 'Add to cart failed: ' + err.message, type: 'error' });
631 } else {
632 alert('Add to cart failed: ' + err.message);
633 }
634 });
635 });
636 }
637})();
638</script>
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help