astro-blog-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,349 @@
1
+ ---
2
+ import type { BlogPostProps } from "../types";
3
+ import { getFeaturedImageUrl } from "../utils/slug";
4
+
5
+ interface Props extends BlogPostProps {}
6
+
7
+ const { post, t } = Astro.props;
8
+
9
+ const imgUrl = getFeaturedImageUrl(post);
10
+ ---
11
+
12
+ <main class="post">
13
+
14
+ <div class="post__hero">
15
+ <div class="post__hero-inner">
16
+ <span class="post__tag">Technical Article</span>
17
+ <h1 class="post__title" set:html={post.title.rendered} />
18
+ <p class="post__date">
19
+ {new Date(post.date).toLocaleDateString("en-US", {
20
+ year: "numeric",
21
+ month: "long",
22
+ day: "numeric",
23
+ })}
24
+ </p>
25
+ </div>
26
+ </div>
27
+
28
+ {imgUrl && (
29
+ <div class="post__image-wrap">
30
+ <img
31
+ src={imgUrl}
32
+ alt={post.title.rendered}
33
+ class="post__image"
34
+ />
35
+ </div>
36
+ )}
37
+
38
+ <div class="post__body">
39
+
40
+ <aside class="post__sidebar">
41
+ <div class="sidebar__block">
42
+ <p class="sidebar__label">Published</p>
43
+ <p class="sidebar__value">
44
+ {new Date(post.date).toLocaleDateString("en-US", {
45
+ month: "short",
46
+ day: "numeric",
47
+ year: "numeric",
48
+ })}
49
+ </p>
50
+ </div>
51
+
52
+ <div class="sidebar__block">
53
+ <p class="sidebar__label">Category</p>
54
+ <p class="sidebar__value">
55
+ {post._embedded?.["wp:term"]?.[0]?.[0]?.name ?? "General"}
56
+ </p>
57
+ </div>
58
+
59
+ {post.readingTime && (
60
+ <div class="sidebar__block">
61
+ <p class="sidebar__label">Reading time</p>
62
+ <p class="sidebar__value">~{post.readingTime} min</p>
63
+ </div>
64
+ )}
65
+
66
+ <div class="sidebar__divider" />
67
+
68
+ <a href="../" class="sidebar__back">
69
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
70
+ <path d="M8.5 2.5L4 7L8.5 11.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
71
+ </svg>
72
+ {t.blog.btn_prev ?? "Back to blog"}
73
+ </a>
74
+ </aside>
75
+
76
+ <article class="post__content" set:html={post.content.rendered} />
77
+
78
+ </div>
79
+
80
+ </main>
81
+
82
+ <style>
83
+ .post {
84
+ background-color: var(--color-bg);
85
+ font-family: var(--font-body);
86
+ min-height: 100vh;
87
+ padding-bottom: 6rem;
88
+ }
89
+
90
+ .post__hero {
91
+ border-bottom: 1px solid var(--color-border);
92
+ padding: 5rem 2rem 3rem;
93
+ }
94
+
95
+ .post__hero-inner {
96
+ max-width: 1100px;
97
+ margin: 0 auto;
98
+ }
99
+
100
+ .post__tag {
101
+ display: inline-block;
102
+ font-family: var(--font-mono);
103
+ font-size: 0.7rem;
104
+ font-weight: 400;
105
+ letter-spacing: 2px;
106
+ text-transform: uppercase;
107
+ color: var(--color-accent);
108
+ border: 1px solid var(--color-accent);
109
+ border-radius: 4px;
110
+ padding: 0.2rem 0.65rem;
111
+ margin-bottom: 1.5rem;
112
+ }
113
+
114
+ .post__title {
115
+ font-family: var(--font-display);
116
+ font-size: clamp(2.4rem, 6vw, 5rem);
117
+ font-weight: 700;
118
+ color: var(--color-text);
119
+ line-height: 1.05;
120
+ letter-spacing: -0.02em;
121
+ margin: 0 0 1.5rem;
122
+ max-width: 820px;
123
+ }
124
+
125
+ .post__date {
126
+ font-family: var(--font-mono);
127
+ font-size: 0.8rem;
128
+ color: var(--color-muted);
129
+ margin: 0;
130
+ }
131
+
132
+ .post__image-wrap {
133
+ max-width: 1100px;
134
+ margin: 2.5rem auto;
135
+ padding: 0 2rem;
136
+ }
137
+
138
+ .post__image {
139
+ width: 100%;
140
+ height: min(55vw, 500px);
141
+ object-fit: cover;
142
+ border-radius: 10px;
143
+ border: 1px solid var(--color-border);
144
+ display: block;
145
+ }
146
+
147
+ .post__body {
148
+ max-width: 1100px;
149
+ margin: 0 auto;
150
+ padding: 0 2rem;
151
+ display: grid;
152
+ grid-template-columns: 180px 1fr;
153
+ gap: 4rem;
154
+ align-items: start;
155
+ }
156
+
157
+ .post__sidebar {
158
+ position: sticky;
159
+ top: 6rem;
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: 1.5rem;
163
+ }
164
+
165
+ .sidebar__block {
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: 0.25rem;
169
+ }
170
+
171
+ .sidebar__label {
172
+ font-family: var(--font-mono);
173
+ font-size: 0.68rem;
174
+ letter-spacing: 1.5px;
175
+ text-transform: uppercase;
176
+ color: var(--color-muted);
177
+ margin: 0;
178
+ }
179
+
180
+ .sidebar__value {
181
+ font-family: var(--font-body);
182
+ font-size: 0.875rem;
183
+ color: var(--color-muted-light);
184
+ margin: 0;
185
+ }
186
+
187
+ .sidebar__divider {
188
+ height: 1px;
189
+ background: var(--color-border);
190
+ margin: 0.5rem 0;
191
+ }
192
+
193
+ .sidebar__back {
194
+ display: inline-flex;
195
+ align-items: center;
196
+ gap: 0.4rem;
197
+ font-family: var(--font-mono);
198
+ font-size: 0.75rem;
199
+ color: var(--color-muted);
200
+ text-decoration: none;
201
+ letter-spacing: 0.5px;
202
+ transition: color 0.15s;
203
+ }
204
+
205
+ .sidebar__back:hover {
206
+ color: var(--color-accent);
207
+ }
208
+
209
+ .post__content {
210
+ color: var(--color-muted-light);
211
+ line-height: 1.85;
212
+ font-size: 1rem;
213
+ font-family: var(--font-body);
214
+ min-width: 0;
215
+ }
216
+
217
+ .post__content :global(h2),
218
+ .post__content :global(h3),
219
+ .post__content :global(h4) {
220
+ font-family: var(--font-display);
221
+ color: var(--color-text);
222
+ font-weight: 700;
223
+ letter-spacing: -0.01em;
224
+ margin: 2.5rem 0 0.9rem;
225
+ line-height: 1.15;
226
+ }
227
+
228
+ .post__content :global(h2) { font-size: 1.75rem; }
229
+ .post__content :global(h3) { font-size: 1.35rem; }
230
+ .post__content :global(h4) { font-size: 1.1rem; }
231
+
232
+ .post__content :global(p) { margin: 0 0 1.25rem; }
233
+
234
+ .post__content :global(strong) {
235
+ color: var(--color-text);
236
+ font-weight: 600;
237
+ }
238
+
239
+ .post__content :global(a) {
240
+ color: var(--color-accent);
241
+ text-decoration: underline;
242
+ text-decoration-thickness: 1px;
243
+ text-underline-offset: 3px;
244
+ }
245
+
246
+ .post__content :global(ul),
247
+ .post__content :global(ol) {
248
+ padding-left: 1.4rem;
249
+ margin: 0 0 1.4rem;
250
+ }
251
+
252
+ .post__content :global(li) { margin-bottom: 0.5rem; }
253
+
254
+ .post__content :global(img) {
255
+ display: block;
256
+ width: 100%;
257
+ border-radius: 10px;
258
+ margin: 2rem 0;
259
+ border: 1px solid var(--color-border);
260
+ }
261
+
262
+ .post__content :global(blockquote) {
263
+ margin: 2rem 0;
264
+ padding: 1rem 1.5rem;
265
+ border-left: 3px solid var(--color-accent);
266
+ background: var(--color-surface);
267
+ color: var(--color-text);
268
+ border-radius: 0 8px 8px 0;
269
+ font-style: italic;
270
+ }
271
+
272
+ .post__content :global(pre) {
273
+ background: var(--color-surface);
274
+ border: 1px solid var(--color-border);
275
+ border-radius: 8px;
276
+ padding: 1.25rem 1.5rem;
277
+ overflow-x: auto;
278
+ margin: 1.75rem 0;
279
+ }
280
+
281
+ .post__content :global(code) {
282
+ font-family: var(--font-mono);
283
+ font-size: 0.875rem;
284
+ color: var(--color-accent);
285
+ background: rgba(56, 189, 248, 0.08);
286
+ padding: 0.15em 0.4em;
287
+ border-radius: 4px;
288
+ }
289
+
290
+ .post__content :global(pre code) {
291
+ background: none;
292
+ padding: 0;
293
+ color: var(--color-muted-light);
294
+ }
295
+
296
+ .post__content :global(table) {
297
+ width: 100%;
298
+ border-collapse: collapse;
299
+ margin: 2rem 0;
300
+ font-size: 0.9rem;
301
+ }
302
+
303
+ .post__content :global(th) {
304
+ background: var(--color-surface);
305
+ color: var(--color-text);
306
+ border: 1px solid var(--color-border);
307
+ padding: 0.75rem 1rem;
308
+ text-align: left;
309
+ font-family: var(--font-mono);
310
+ font-size: 0.75rem;
311
+ letter-spacing: 0.5px;
312
+ text-transform: uppercase;
313
+ }
314
+
315
+ .post__content :global(td) {
316
+ border: 1px solid var(--color-border);
317
+ padding: 0.75rem 1rem;
318
+ color: var(--color-muted-light);
319
+ }
320
+
321
+ @media (max-width: 768px) {
322
+ .post__hero { padding: 4.5rem 1.25rem 2.5rem; }
323
+
324
+ .post__image-wrap {
325
+ padding: 0 1.25rem;
326
+ margin: 2rem auto;
327
+ }
328
+
329
+ .post__image { height: 240px; }
330
+
331
+ .post__body {
332
+ grid-template-columns: 1fr;
333
+ gap: 2rem;
334
+ padding: 0 1.25rem;
335
+ }
336
+
337
+ .post__sidebar {
338
+ position: static;
339
+ flex-direction: row;
340
+ flex-wrap: wrap;
341
+ gap: 1rem 2rem;
342
+ padding-bottom: 1.5rem;
343
+ border-bottom: 1px solid var(--color-border);
344
+ }
345
+
346
+ .sidebar__divider,
347
+ .sidebar__back { display: none; }
348
+ }
349
+ </style>
@@ -0,0 +1,331 @@
1
+ ---
2
+ interface Props {
3
+ postId: number;
4
+ apiRoute?: string;
5
+ replyToId?: number;
6
+ }
7
+
8
+ const { postId, apiRoute = "/api/comments", replyToId } = Astro.props;
9
+ ---
10
+
11
+ <section class="comment-form" id="comment-form">
12
+ <h3 class="comment-form__title">Leave a Comment</h3>
13
+
14
+ <div class="comment-form__status comment-form__status--success" id="cf-success" aria-live="polite">
15
+ ✓ Your comment has been submitted and is awaiting moderation.
16
+ </div>
17
+
18
+ <div class="comment-form__status comment-form__status--error" id="cf-error" aria-live="polite">
19
+ Something went wrong. Please try again.
20
+ </div>
21
+
22
+ <form id="cf-form" novalidate>
23
+ <input type="hidden" name="post" value={postId} />
24
+ <input type="hidden" name="parent" id="cf-parent" value={replyToId ?? 0} />
25
+
26
+ <div class="comment-form__reply-indicator" id="cf-reply-indicator">
27
+ Replying to <strong id="cf-reply-name"></strong>
28
+ <button type="button" id="cf-cancel-reply">✕ Cancel</button>
29
+ </div>
30
+
31
+ <div class="comment-form__row">
32
+ <div class="comment-form__field">
33
+ <label for="cf-name" class="comment-form__label">
34
+ Name <span aria-hidden="true">*</span>
35
+ </label>
36
+ <input
37
+ type="text"
38
+ id="cf-name"
39
+ name="author_name"
40
+ class="comment-form__input"
41
+ required
42
+ autocomplete="name"
43
+ placeholder="Your name"
44
+ />
45
+ </div>
46
+
47
+ <div class="comment-form__field">
48
+ <label for="cf-email" class="comment-form__label">
49
+ Email <span aria-hidden="true">*</span>
50
+ </label>
51
+ <input
52
+ type="email"
53
+ id="cf-email"
54
+ name="author_email"
55
+ class="comment-form__input"
56
+ required
57
+ autocomplete="email"
58
+ placeholder="your@email.com"
59
+ />
60
+ <p class="comment-form__hint">Your email will not be published.</p>
61
+ </div>
62
+ </div>
63
+
64
+ <div class="comment-form__field">
65
+ <label for="cf-content" class="comment-form__label">
66
+ Comment <span aria-hidden="true">*</span>
67
+ </label>
68
+ <textarea
69
+ id="cf-content"
70
+ name="content"
71
+ class="comment-form__textarea"
72
+ required
73
+ rows="5"
74
+ placeholder="Write your comment here..."
75
+ ></textarea>
76
+ </div>
77
+
78
+ <button type="submit" class="comment-form__submit" id="cf-submit">
79
+ <span id="cf-submit-text">Post Comment</span>
80
+ <span id="cf-submit-loading" aria-hidden="true">Sending...</span>
81
+ </button>
82
+ </form>
83
+ </section>
84
+
85
+ <script define:vars={{ apiRoute }}>
86
+ const form = document.getElementById("cf-form");
87
+ const submitBtn = document.getElementById("cf-submit");
88
+ const submitText = document.getElementById("cf-submit-text");
89
+ const submitLoading = document.getElementById("cf-submit-loading");
90
+ const successMsg = document.getElementById("cf-success");
91
+ const errorMsg = document.getElementById("cf-error");
92
+ const replyIndicator = document.getElementById("cf-reply-indicator");
93
+ const replyName = document.getElementById("cf-reply-name");
94
+ const cancelReply = document.getElementById("cf-cancel-reply");
95
+ const parentInput = document.getElementById("cf-parent");
96
+
97
+ // Maneja clicks en botones de reply desde Comments.astro
98
+ document.querySelectorAll("[data-reply-to]").forEach((btn) => {
99
+ btn.addEventListener("click", () => {
100
+ const replyTo = btn.getAttribute("data-reply-to");
101
+ const name = btn.getAttribute("data-reply-name");
102
+
103
+ parentInput.value = replyTo;
104
+ replyName.textContent = name;
105
+ replyIndicator.classList.add("comment-form__reply-indicator--active");
106
+
107
+ // Scroll al form
108
+ document.getElementById("comment-form").scrollIntoView({
109
+ behavior: "smooth",
110
+ block: "start",
111
+ });
112
+ });
113
+ });
114
+
115
+ // Cancela reply
116
+ cancelReply.addEventListener("click", () => {
117
+ parentInput.value = "0";
118
+ replyIndicator.classList.remove("comment-form__reply-indicator--active");
119
+ });
120
+
121
+ // Submit
122
+ form.addEventListener("submit", async (e) => {
123
+ e.preventDefault();
124
+
125
+ // Reset mensajes
126
+ successMsg.style.display = "none";
127
+ errorMsg.style.display = "none";
128
+
129
+ // Loading state
130
+ submitBtn.disabled = true;
131
+ submitText.style.display = "none";
132
+ submitLoading.style.display = "inline";
133
+
134
+ const formData = new FormData(form);
135
+ const payload = {
136
+ post: Number(formData.get("post")),
137
+ parent: Number(formData.get("parent")) || 0,
138
+ author_name: formData.get("author_name"),
139
+ author_email: formData.get("author_email"),
140
+ content: formData.get("content"),
141
+ };
142
+
143
+ try {
144
+ const res = await fetch(apiRoute, {
145
+ method: "POST",
146
+ headers: { "Content-Type": "application/json" },
147
+ body: JSON.stringify(payload),
148
+ });
149
+
150
+ if (!res.ok) throw new Error("Request failed");
151
+
152
+ // Éxito
153
+ successMsg.style.display = "block";
154
+ form.reset();
155
+ parentInput.value = "0";
156
+ replyIndicator.classList.remove("comment-form__reply-indicator--active");
157
+
158
+ } catch {
159
+ errorMsg.style.display = "block";
160
+ } finally {
161
+ submitBtn.disabled = false;
162
+ submitText.style.display = "inline";
163
+ submitLoading.style.display = "none";
164
+ }
165
+ });
166
+ </script>
167
+
168
+ <style>
169
+ .comment-form {
170
+ max-width: 720px;
171
+ margin: 3rem auto 0;
172
+ padding: 0 2rem 4rem;
173
+ font-family: var(--font-body);
174
+ }
175
+
176
+ .comment-form__title {
177
+ font-family: var(--font-display);
178
+ font-size: 1.35rem;
179
+ font-weight: 700;
180
+ color: var(--color-text);
181
+ margin: 0 0 1.5rem;
182
+ }
183
+
184
+ .comment-form__status {
185
+ display: none;
186
+ padding: 0.75rem 1rem;
187
+ border-radius: 6px;
188
+ font-size: 0.9rem;
189
+ margin-bottom: 1.25rem;
190
+ }
191
+
192
+ .comment-form__status--success {
193
+ background: rgba(34, 197, 94, 0.1);
194
+ color: #16a34a;
195
+ border: 1px solid rgba(34, 197, 94, 0.3);
196
+ }
197
+
198
+ .comment-form__status--error {
199
+ background: rgba(239, 68, 68, 0.1);
200
+ color: #dc2626;
201
+ border: 1px solid rgba(239, 68, 68, 0.3);
202
+ }
203
+
204
+ .comment-form__reply-indicator {
205
+ display: none;
206
+ align-items: center;
207
+ gap: 0.5rem;
208
+ font-size: 0.85rem;
209
+ color: var(--color-muted);
210
+ background: var(--color-surface);
211
+ border: 1px solid var(--color-border);
212
+ border-radius: 6px;
213
+ padding: 0.5rem 0.75rem;
214
+ margin-bottom: 1rem;
215
+ }
216
+
217
+ .comment-form__reply-indicator--active {
218
+ display: flex;
219
+ }
220
+
221
+ .comment-form__reply-indicator button {
222
+ margin-left: auto;
223
+ background: none;
224
+ border: none;
225
+ cursor: pointer;
226
+ font-size: 0.8rem;
227
+ color: var(--color-muted);
228
+ padding: 0;
229
+ }
230
+
231
+ .comment-form__reply-indicator button:hover {
232
+ color: var(--color-accent);
233
+ }
234
+
235
+ .comment-form__row {
236
+ display: grid;
237
+ grid-template-columns: 1fr 1fr;
238
+ gap: 1rem;
239
+ margin-bottom: 1rem;
240
+ }
241
+
242
+ @media (max-width: 600px) {
243
+ .comment-form__row {
244
+ grid-template-columns: 1fr;
245
+ }
246
+ }
247
+
248
+ .comment-form__field {
249
+ display: flex;
250
+ flex-direction: column;
251
+ gap: 0.4rem;
252
+ }
253
+
254
+ .comment-form__label {
255
+ font-size: 0.8rem;
256
+ font-weight: 600;
257
+ color: var(--color-text);
258
+ font-family: var(--font-mono);
259
+ letter-spacing: 0.5px;
260
+ text-transform: uppercase;
261
+ }
262
+
263
+ .comment-form__label span {
264
+ color: var(--color-accent);
265
+ }
266
+
267
+ .comment-form__input,
268
+ .comment-form__textarea {
269
+ background: var(--color-surface);
270
+ border: 1px solid var(--color-border);
271
+ border-radius: 6px;
272
+ padding: 0.65rem 0.9rem;
273
+ font-size: 0.95rem;
274
+ font-family: var(--font-body);
275
+ color: var(--color-text);
276
+ transition: border-color 0.15s;
277
+ width: 100%;
278
+ box-sizing: border-box;
279
+ }
280
+
281
+ .comment-form__input:focus,
282
+ .comment-form__textarea:focus {
283
+ outline: none;
284
+ border-color: var(--color-accent);
285
+ }
286
+
287
+ .comment-form__textarea {
288
+ resize: vertical;
289
+ min-height: 120px;
290
+ }
291
+
292
+ .comment-form__hint {
293
+ font-size: 0.75rem;
294
+ color: var(--color-muted);
295
+ margin: 0;
296
+ }
297
+
298
+ .comment-form__submit {
299
+ margin-top: 1.25rem;
300
+ padding: 0.7rem 1.75rem;
301
+ background: var(--color-accent);
302
+ color: var(--color-bg);
303
+ border: none;
304
+ border-radius: 6px;
305
+ font-size: 0.9rem;
306
+ font-weight: 700;
307
+ font-family: var(--font-mono);
308
+ letter-spacing: 0.5px;
309
+ cursor: pointer;
310
+ transition: opacity 0.15s;
311
+ }
312
+
313
+ .comment-form__submit:hover {
314
+ opacity: 0.85;
315
+ }
316
+
317
+ .comment-form__submit:disabled {
318
+ opacity: 0.5;
319
+ cursor: not-allowed;
320
+ }
321
+
322
+ #cf-submit-loading {
323
+ display: none;
324
+ }
325
+
326
+ @media (max-width: 768px) {
327
+ .comment-form {
328
+ padding: 0 1.25rem 4rem;
329
+ }
330
+ }
331
+ </style>