source: Klonkt/src/views/partials/post-card.ejs@ 7bc636b

main
Last change on this file since 7bc636b was 7bc636b, checked in by Robin <robin@…>, 4 months ago

Initial commit — PrutFolio v1 source (pulled from Hetzner /srv/prutfolio)

  • Property mode set to 100644
File size: 6.7 KB
Line 
1<%
2// Post list item — MOBILE-FIRST.
3//
4// <768px (default):
5// Vertical stack — full-bleed 16:9 cover on top, content below.
6// Whole card is tappable (one big <a> wrapping everything).
7// Active state: scale(0.985) + opacity dim.
8//
9// ≥768px (desktop):
10// v9 pattern — 120px square cover left, content right.
11// Hover state on cover. Tap-target stays large.
12
13const _href = '/' + post.slug;
14const _hasCover = !!post.cover_image_url;
15const _typeLabel = (post.type && post.type !== 'overig' && post.type !== 'post') ? post.type : '';
16const _isPinned = !!post.pinned;
17const _isDraft = post.status && post.status !== 'published';
18
19function _ddmmyyyy(iso) {
20 if (!iso) return '';
21 const d = new Date(iso);
22 if (isNaN(d.getTime())) return '';
23 const pad = n => String(n).padStart(2, '0');
24 return pad(d.getDate()) + '-' + pad(d.getMonth() + 1) + '-' + d.getFullYear();
25}
26
27let _tags = [];
28if (post.tags) {
29 if (Array.isArray(post.tags)) _tags = post.tags;
30 else if (typeof post.tags === 'string') {
31 try {
32 const parsed = JSON.parse(post.tags);
33 _tags = Array.isArray(parsed) ? parsed : post.tags.split(',').map(t => t.trim()).filter(Boolean);
34 } catch (e) {
35 _tags = post.tags.split(',').map(t => t.trim()).filter(Boolean);
36 }
37 }
38}
39%>
40<li class="post-list-item<%= _hasCover ? ' has-cover' : '' %><%= _isPinned ? ' is-pinned' : '' %>">
41
42 <% if (_hasCover) { %>
43 <a class="post-list-cover" href="<%= _href %>"
44 hx-get="<%= _href %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
45 hx-push-url="<%= _href %>" hx-indicator="#pcms-loading"
46 aria-label="<%= post.title || '(zonder titel)' %>" tabindex="-1">
47 <img src="<%= post.cover_image_url %>" alt="" loading="lazy" decoding="async">
48 </a>
49 <% } %>
50
51 <div class="post-list-body">
52 <div class="post-list-meta mono">
53 <% if (_isPinned) { %><span class="post-list-pin" aria-label="Vastgepind">★</span><% } %>
54 <time datetime="<%= post.published_at || post.created_at %>"><%= _ddmmyyyy(post.published_at || post.created_at) %></time>
55 <% if (_typeLabel) { %>
56 <span class="post-list-type">· <%= _typeLabel.charAt(0).toUpperCase() + _typeLabel.slice(1) %></span>
57 <% } %>
58 <% if (_isDraft) { %>
59 <span class="post-list-type post-list-draft">· concept</span>
60 <% } %>
61 </div>
62
63 <h3 class="post-list-title">
64 <a href="<%= _href %>"
65 hx-get="<%= _href %>?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
66 hx-push-url="<%= _href %>" hx-indicator="#pcms-loading">
67 <%= post.title || '(zonder titel)' %>
68 </a>
69 </h3>
70
71 <% if (post.excerpt) { %>
72 <p class="post-list-excerpt"><%= post.excerpt %></p>
73 <% } %>
74
75 <% if (_tags.length) { %>
76 <div class="post-list-tags">
77 <% _tags.forEach(function(t) { %>
78 <a href="/tag/<%= encodeURIComponent(t) %>" class="tag-chip"
79 hx-get="/tag/<%= encodeURIComponent(t) %>?partial=1" hx-target="#pcms-main"
80 hx-swap="innerHTML" hx-push-url="/tag/<%= encodeURIComponent(t) %>"
81 hx-indicator="#pcms-loading">#<%= t %></a>
82 <% }); %>
83 </div>
84 <% } %>
85 </div>
86</li>
87
88<style>
89/* ============================================================
90 POST-LIST-ITEM — MOBILE-FIRST OVERRIDE
91 The base v9 styles in /assets/css/style.css use a desktop
92 120×120 grid by default. We flip that: stacked on mobile,
93 side-by-side on ≥768px.
94 ============================================================ */
95
96/* Reset v9's grid on mobile and stack vertically */
97@media (max-width: 767px) {
98 .post-list {
99 gap: 2rem;
100 }
101 .post-list-item {
102 display: flex !important;
103 flex-direction: column;
104 grid-template-columns: none !important;
105 gap: .85rem !important;
106 padding-bottom: 2rem;
107 /* Active feedback for the whole card row */
108 transition: transform 120ms ease, opacity 120ms ease;
109 }
110 .post-list-item:active {
111 transform: scale(0.985);
112 opacity: 0.85;
113 }
114 .post-list-item:last-child {
115 border-bottom: none;
116 padding-bottom: 0;
117 }
118
119 /* Cover: full-bleed of the container, 16:9 ratio, no border-radius
120 on small phones (edge-to-edge feels app-native). 8px radius from 480px+. */
121 .post-list-cover {
122 aspect-ratio: 16 / 9 !important;
123 width: 100% !important;
124 border-radius: 0 !important;
125 /* Negative margin to break out of .container's padding */
126 margin-left: calc(-1 * clamp(1.25rem, 4vw, 2rem));
127 margin-right: calc(-1 * clamp(1.25rem, 4vw, 2rem));
128 width: calc(100% + 2 * clamp(1.25rem, 4vw, 2rem)) !important;
129 background: var(--paper-2);
130 }
131 .post-list-cover img {
132 width: 100%; height: 100%;
133 object-fit: cover;
134 display: block;
135 }
136
137 /* Re-apply rounded corners on slightly larger phones */
138 @media (min-width: 480px) {
139 .post-list-cover {
140 border-radius: var(--radius) !important;
141 margin-left: 0 !important;
142 margin-right: 0 !important;
143 width: 100% !important;
144 }
145 }
146
147 /* Larger title for mobile readability */
148 .post-list-title {
149 font-size: 1.45rem !important;
150 line-height: 1.2 !important;
151 }
152 .post-list-excerpt {
153 font-size: 1rem !important;
154 color: var(--ink-soft) !important;
155 line-height: 1.55 !important;
156 }
157 .post-list-meta {
158 font-size: .8rem !important;
159 gap: .5rem !important;
160 }
161
162 /* Pinned indicator */
163 .post-list-item.is-pinned .post-list-title a {
164 /* No visual change on title — the ★ in meta is enough */
165 }
166
167 /* Tap targets for tags */
168 .post-list-tags { gap: .4rem; }
169 .tag-chip {
170 min-height: 32px;
171 display: inline-flex;
172 align-items: center;
173 padding: .2rem .65rem;
174 }
175}
176
177/* ============================================================
178 DESKTOP — keep v9's 120px-square pattern (style.css does this)
179 We just enforce the layout here for clarity + override safety.
180 ============================================================ */
181@media (min-width: 768px) {
182 .post-list-item.has-cover {
183 display: grid !important;
184 grid-template-columns: 120px 1fr !important;
185 gap: 1.25rem !important;
186 }
187 .post-list-item:not(.has-cover) {
188 grid-template-columns: 1fr !important;
189 }
190 .post-list-cover {
191 aspect-ratio: 1 !important;
192 border-radius: var(--radius) !important;
193 }
194 /* Hover lift on desktop (touch devices use :active above) */
195 @media (hover: hover) {
196 .post-list-cover {
197 transition: transform 200ms ease;
198 }
199 .post-list-item:hover .post-list-cover {
200 transform: scale(1.02);
201 }
202 }
203}
204
205/* ============================================================
206 PIN + DRAFT INDICATORS (shared)
207 ============================================================ */
208.post-list-pin {
209 color: var(--accent);
210 margin-right: .35rem;
211 font-size: .9rem;
212}
213.post-list-draft {
214 color: var(--accent);
215 font-style: italic;
216}
217</style>
Note: See TracBrowser for help on using the repository browser.