@commonpub/layer 0.8.3 → 0.8.5

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.
Files changed (79) hide show
  1. package/components/ContentCard.vue +1 -1
  2. package/components/ImageUpload.vue +1 -1
  3. package/components/ShareToHubModal.vue +1 -1
  4. package/components/blocks/BlockCodeView.vue +26 -25
  5. package/components/contest/ContestEntries.vue +112 -0
  6. package/components/contest/ContestHero.vue +204 -0
  7. package/components/contest/ContestJudges.vue +51 -0
  8. package/components/contest/ContestPrizes.vue +82 -0
  9. package/components/contest/ContestRules.vue +34 -0
  10. package/components/contest/ContestSidebar.vue +83 -0
  11. package/components/editors/BlogEditor.vue +1 -1
  12. package/components/editors/DocsPageTree.vue +10 -0
  13. package/components/hub/HubHero.vue +1 -1
  14. package/composables/useSanitize.ts +112 -9
  15. package/composables/useTheme.ts +8 -0
  16. package/layouts/default.vue +7 -7
  17. package/middleware/feature-gate.global.ts +24 -0
  18. package/package.json +6 -6
  19. package/pages/[type]/index.vue +4 -3
  20. package/pages/admin/audit.vue +3 -2
  21. package/pages/admin/federation.vue +33 -13
  22. package/pages/admin/index.vue +7 -1
  23. package/pages/admin/reports.vue +152 -36
  24. package/pages/admin/settings.vue +17 -5
  25. package/pages/admin/theme.vue +5 -3
  26. package/pages/auth/forgot-password.vue +35 -35
  27. package/pages/auth/login.vue +6 -5
  28. package/pages/auth/reset-password.vue +44 -32
  29. package/pages/contests/[slug]/edit.vue +238 -56
  30. package/pages/contests/[slug]/index.vue +54 -450
  31. package/pages/contests/[slug]/judge.vue +141 -53
  32. package/pages/contests/[slug]/results.vue +182 -0
  33. package/pages/contests/create.vue +64 -64
  34. package/pages/contests/index.vue +2 -1
  35. package/pages/docs/[siteSlug]/[...pagePath].vue +6 -5
  36. package/pages/docs/[siteSlug]/edit.vue +58 -2
  37. package/pages/docs/[siteSlug]/index.vue +6 -5
  38. package/pages/federated-hubs/[id]/posts/[postId].vue +2 -2
  39. package/pages/hubs/index.vue +3 -2
  40. package/pages/index.vue +25 -7
  41. package/pages/learn/index.vue +1 -1
  42. package/pages/mirror/[id].vue +3 -3
  43. package/pages/notifications.vue +15 -1
  44. package/pages/products/[slug].vue +5 -2
  45. package/pages/settings/notifications.vue +7 -1
  46. package/pages/tags/[slug].vue +3 -2
  47. package/pages/tags/index.vue +3 -2
  48. package/pages/videos/[id].vue +18 -0
  49. package/server/api/admin/content/[id].patch.ts +1 -1
  50. package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +1 -1
  51. package/server/api/admin/federation/refederate.post.ts +7 -3
  52. package/server/api/admin/federation/repair-types.post.ts +2 -45
  53. package/server/api/admin/federation/retry.post.ts +7 -4
  54. package/server/api/admin/reports.get.ts +1 -0
  55. package/server/api/auth/federated/login.post.ts +22 -2
  56. package/server/api/auth/sign-in-username.post.ts +42 -0
  57. package/server/api/content/[id]/products-sync.post.ts +7 -6
  58. package/server/api/contests/[slug]/entries/[entryId].delete.ts +14 -0
  59. package/server/api/contests/[slug]/entries.get.ts +6 -1
  60. package/server/api/contests/[slug]/judge.post.ts +8 -2
  61. package/server/api/docs/[siteSlug]/nav.get.ts +1 -1
  62. package/server/api/docs/[siteSlug]/pages/[pageId]/duplicate.post.ts +16 -0
  63. package/server/api/docs/[siteSlug]/pages/reorder.post.ts +4 -1
  64. package/server/api/docs/migrate-content.post.ts +1 -7
  65. package/server/api/federation/hub-follow-status.get.ts +2 -18
  66. package/server/api/federation/hub-follow.post.ts +9 -27
  67. package/server/api/federation/hub-post-like.post.ts +9 -98
  68. package/server/api/federation/hub-post-likes.get.ts +3 -13
  69. package/server/api/notifications/read.post.ts +6 -1
  70. package/server/api/profile/theme.put.ts +23 -0
  71. package/server/api/search/index.get.ts +2 -2
  72. package/server/api/search/trending.get.ts +3 -3
  73. package/server/api/users/index.get.ts +9 -2
  74. package/server/middleware/content-ap.ts +2 -2
  75. package/server/routes/.well-known/webfinger.ts +2 -2
  76. package/theme/base.css +23 -0
  77. package/components/EditorPropertiesPanel.vue +0 -393
  78. package/components/views/BlogView.vue +0 -735
  79. package/server/api/resolve-identity.post.ts +0 -34
@@ -1,393 +0,0 @@
1
- <script setup lang="ts">
2
- const props = defineProps<{
3
- contentType: string;
4
- metadata: Record<string, unknown>;
5
- selectedBlock: { type: string; attrs: Record<string, unknown> } | null;
6
- }>();
7
-
8
- const emit = defineEmits<{
9
- 'update:metadata': [metadata: Record<string, unknown>];
10
- 'slug-edited': [];
11
- }>();
12
-
13
- function updateField(key: string, value: unknown): void {
14
- emit('update:metadata', { ...props.metadata, [key]: value });
15
- }
16
-
17
- const visibilityOptions = ['public', 'members', 'private'];
18
-
19
- // Fetch user's hubs for the hub assignment dropdown
20
- const { isAuthenticated } = useAuth();
21
- const { data: userHubs } = useLazyFetch<{ items: Array<{ id: string; name: string; slug: string; role: string }> }>('/api/user/hubs', {
22
- immediate: isAuthenticated.value,
23
- default: () => ({ items: [] }),
24
- });
25
- const difficultyOptions = [
26
- { value: 1, label: 'Beginner' },
27
- { value: 2, label: 'Intermediate' },
28
- { value: 3, label: 'Advanced' },
29
- ];
30
- </script>
31
-
32
- <template>
33
- <aside class="cpub-properties" aria-label="Document properties">
34
- <div class="cpub-properties-header">
35
- <i class="fa-solid fa-sliders"></i>
36
- <span class="cpub-properties-title">Properties</span>
37
- </div>
38
-
39
- <div class="cpub-properties-body">
40
- <!-- Document properties -->
41
- <section class="cpub-prop-section">
42
- <span class="cpub-prop-section-label"><i class="fa-solid fa-file-lines"></i> Document</span>
43
-
44
- <div class="cpub-prop-field">
45
- <label for="prop-slug" class="cpub-prop-label">Slug</label>
46
- <input
47
- id="prop-slug"
48
- type="text"
49
- class="cpub-prop-input"
50
- :value="metadata.slug"
51
- placeholder="auto-generated from title"
52
- @input="updateField('slug', ($event.target as HTMLInputElement).value); emit('slug-edited')"
53
- />
54
- </div>
55
-
56
- <div class="cpub-prop-field">
57
- <label for="prop-description" class="cpub-prop-label">Description</label>
58
- <textarea
59
- id="prop-description"
60
- class="cpub-prop-textarea"
61
- rows="3"
62
- :value="metadata.description as string"
63
- placeholder="Brief description..."
64
- @input="updateField('description', ($event.target as HTMLTextAreaElement).value)"
65
- />
66
- </div>
67
-
68
- <div class="cpub-prop-field">
69
- <label for="prop-tags" class="cpub-prop-label">Tags</label>
70
- <input
71
- id="prop-tags"
72
- type="text"
73
- class="cpub-prop-input"
74
- :value="(metadata.tags as string[] || []).join(', ')"
75
- placeholder="tag1, tag2, tag3"
76
- @input="updateField('tags', ($event.target as HTMLInputElement).value.split(',').map(t => t.trim()).filter(Boolean))"
77
- />
78
- </div>
79
-
80
- <div class="cpub-prop-field">
81
- <label for="prop-visibility" class="cpub-prop-label">Visibility</label>
82
- <select
83
- id="prop-visibility"
84
- class="cpub-prop-select"
85
- :value="metadata.visibility || 'public'"
86
- @change="updateField('visibility', ($event.target as HTMLSelectElement).value)"
87
- >
88
- <option v-for="opt in visibilityOptions" :key="opt" :value="opt">{{ opt }}</option>
89
- </select>
90
- </div>
91
-
92
- <div v-if="userHubs?.items?.length" class="cpub-prop-field">
93
- <label for="prop-hub" class="cpub-prop-label">Community</label>
94
- <select
95
- id="prop-hub"
96
- class="cpub-prop-select"
97
- :value="metadata.hubSlug || ''"
98
- @change="updateField('hubSlug', ($event.target as HTMLSelectElement).value || undefined)"
99
- >
100
- <option value="">None</option>
101
- <option v-for="hub in userHubs.items" :key="hub.id" :value="hub.slug">{{ hub.name }}</option>
102
- </select>
103
- <span class="cpub-prop-hint">Link to a community's project gallery. Content visibility is controlled separately above.</span>
104
- </div>
105
-
106
- <div class="cpub-prop-field">
107
- <label for="prop-cover" class="cpub-prop-label">Cover Image</label>
108
- <input
109
- id="prop-cover"
110
- type="text"
111
- class="cpub-prop-input"
112
- :value="metadata.coverImage"
113
- placeholder="URL or upload..."
114
- @input="updateField('coverImage', ($event.target as HTMLInputElement).value)"
115
- />
116
- </div>
117
- </section>
118
-
119
- <!-- Type-specific metadata -->
120
- <section v-if="contentType === 'article' || contentType === 'blog'" class="cpub-prop-section">
121
- <span class="cpub-prop-section-label"><i :class="contentType === 'article' ? 'fa-solid fa-newspaper' : 'fa-solid fa-pen-nib'"></i> {{ contentType === 'article' ? 'Article' : 'Blog' }}</span>
122
-
123
- <div class="cpub-prop-field">
124
- <label for="prop-category" class="cpub-prop-label">Category</label>
125
- <input
126
- id="prop-category"
127
- type="text"
128
- class="cpub-prop-input"
129
- :value="metadata.category"
130
- placeholder="Category..."
131
- @input="updateField('category', ($event.target as HTMLInputElement).value)"
132
- />
133
- </div>
134
-
135
- <div v-if="contentType === 'blog'" class="cpub-prop-field">
136
- <label for="prop-series" class="cpub-prop-label">Series</label>
137
- <input
138
- id="prop-series"
139
- type="text"
140
- class="cpub-prop-input"
141
- :value="metadata.series"
142
- placeholder="Series name..."
143
- @input="updateField('series', ($event.target as HTMLInputElement).value)"
144
- />
145
- </div>
146
-
147
- <div class="cpub-prop-field">
148
- <label for="prop-seo" class="cpub-prop-label">SEO Description</label>
149
- <textarea
150
- id="prop-seo"
151
- class="cpub-prop-textarea"
152
- rows="2"
153
- :value="metadata.seoDescription as string"
154
- placeholder="SEO description..."
155
- @input="updateField('seoDescription', ($event.target as HTMLTextAreaElement).value)"
156
- />
157
- </div>
158
- </section>
159
-
160
- <section v-if="contentType === 'project'" class="cpub-prop-section">
161
- <span class="cpub-prop-section-label"><i class="fa-solid fa-microchip"></i> Project</span>
162
-
163
- <div class="cpub-prop-field">
164
- <label for="prop-difficulty" class="cpub-prop-label">Difficulty</label>
165
- <select
166
- id="prop-difficulty"
167
- class="cpub-prop-select"
168
- :value="metadata.difficulty || 1"
169
- @change="updateField('difficulty', Number(($event.target as HTMLSelectElement).value))"
170
- >
171
- <option v-for="opt in difficultyOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
172
- </select>
173
- </div>
174
-
175
- <div class="cpub-prop-field">
176
- <label for="prop-buildtime" class="cpub-prop-label">Build Time</label>
177
- <input
178
- id="prop-buildtime"
179
- type="text"
180
- class="cpub-prop-input"
181
- :value="metadata.buildTime"
182
- placeholder="e.g., 2 hours"
183
- @input="updateField('buildTime', ($event.target as HTMLInputElement).value)"
184
- />
185
- </div>
186
-
187
- <div class="cpub-prop-field">
188
- <label for="prop-cost" class="cpub-prop-label">Estimated Cost</label>
189
- <input
190
- id="prop-cost"
191
- type="text"
192
- class="cpub-prop-input"
193
- :value="metadata.estimatedCost"
194
- placeholder="e.g., $50"
195
- @input="updateField('estimatedCost', ($event.target as HTMLInputElement).value)"
196
- />
197
- </div>
198
- </section>
199
-
200
- <section v-if="contentType === 'explainer'" class="cpub-prop-section">
201
- <span class="cpub-prop-section-label"><i class="fa-solid fa-lightbulb"></i> Explainer</span>
202
-
203
- <div class="cpub-prop-field">
204
- <label for="prop-exp-difficulty" class="cpub-prop-label">Difficulty</label>
205
- <select
206
- id="prop-exp-difficulty"
207
- class="cpub-prop-select"
208
- :value="metadata.difficulty || 1"
209
- @change="updateField('difficulty', Number(($event.target as HTMLSelectElement).value))"
210
- >
211
- <option v-for="opt in difficultyOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
212
- </select>
213
- </div>
214
-
215
- <div class="cpub-prop-field">
216
- <label for="prop-minutes" class="cpub-prop-label">Est. Minutes</label>
217
- <input
218
- id="prop-minutes"
219
- type="number"
220
- class="cpub-prop-input"
221
- :value="metadata.estimatedMinutes"
222
- placeholder="10"
223
- @input="updateField('estimatedMinutes', Number(($event.target as HTMLInputElement).value))"
224
- />
225
- </div>
226
-
227
- <div class="cpub-prop-field">
228
- <label for="prop-objectives" class="cpub-prop-label">Learning Objectives</label>
229
- <textarea
230
- id="prop-objectives"
231
- class="cpub-prop-textarea"
232
- rows="3"
233
- :value="metadata.learningObjectives as string"
234
- placeholder="One per line..."
235
- @input="updateField('learningObjectives', ($event.target as HTMLTextAreaElement).value)"
236
- />
237
- </div>
238
- </section>
239
-
240
- <!-- Selected block properties -->
241
- <section v-if="selectedBlock" class="cpub-prop-section">
242
- <span class="cpub-prop-section-label"><i class="fa-solid fa-cube"></i> Block: {{ selectedBlock.type }}</span>
243
-
244
- <template v-if="selectedBlock.type === 'image'">
245
- <div class="cpub-prop-field">
246
- <label class="cpub-prop-label">Alt Text</label>
247
- <input type="text" class="cpub-prop-input" :value="selectedBlock.attrs.alt" readonly />
248
- </div>
249
- <div class="cpub-prop-field">
250
- <label class="cpub-prop-label">Caption</label>
251
- <input type="text" class="cpub-prop-input" :value="selectedBlock.attrs.caption" readonly />
252
- </div>
253
- </template>
254
-
255
- <template v-if="selectedBlock.type === 'code_block'">
256
- <div class="cpub-prop-field">
257
- <label class="cpub-prop-label">Language</label>
258
- <input type="text" class="cpub-prop-input" :value="selectedBlock.attrs.language" readonly />
259
- </div>
260
- <div class="cpub-prop-field">
261
- <label class="cpub-prop-label">Filename</label>
262
- <input type="text" class="cpub-prop-input" :value="selectedBlock.attrs.filename" readonly />
263
- </div>
264
- </template>
265
-
266
- <template v-if="selectedBlock.type === 'callout'">
267
- <div class="cpub-prop-field">
268
- <label class="cpub-prop-label">Variant</label>
269
- <span class="cpub-prop-value">{{ selectedBlock.attrs.variant }}</span>
270
- </div>
271
- </template>
272
- </section>
273
- </div>
274
- </aside>
275
- </template>
276
-
277
- <style scoped>
278
- .cpub-properties {
279
- width: 280px;
280
- border-left: var(--border-width-default) solid var(--border);
281
- background: var(--surface);
282
- overflow-y: auto;
283
- flex-shrink: 0;
284
- }
285
-
286
- .cpub-properties-header {
287
- padding: var(--space-3) var(--space-4);
288
- border-bottom: var(--border-width-default) solid var(--border2);
289
- display: flex;
290
- align-items: center;
291
- gap: 8px;
292
- color: var(--text-dim);
293
- }
294
-
295
- .cpub-properties-header i {
296
- font-size: 14px;
297
- }
298
-
299
- .cpub-properties-title {
300
- font-family: var(--font-mono);
301
- font-size: var(--text-label);
302
- font-weight: var(--font-weight-semibold);
303
- text-transform: uppercase;
304
- letter-spacing: var(--tracking-widest);
305
- color: var(--text-dim);
306
- }
307
-
308
- .cpub-properties-body {
309
- padding: var(--space-3);
310
- }
311
-
312
- .cpub-prop-section {
313
- margin-bottom: var(--space-4);
314
- padding-bottom: var(--space-4);
315
- border-bottom: var(--border-width-default) solid var(--border2);
316
- }
317
-
318
- .cpub-prop-section:last-child {
319
- border-bottom: none;
320
- }
321
-
322
- .cpub-prop-section-label {
323
- display: flex;
324
- align-items: center;
325
- gap: 8px;
326
- font-family: var(--font-mono);
327
- font-size: 10px;
328
- font-weight: var(--font-weight-semibold);
329
- text-transform: uppercase;
330
- letter-spacing: var(--tracking-widest);
331
- color: var(--text-faint);
332
- margin-bottom: var(--space-3);
333
- }
334
-
335
- .cpub-prop-section-label i {
336
- font-size: 12px;
337
- color: var(--accent);
338
- }
339
-
340
- .cpub-prop-field {
341
- margin-bottom: var(--space-3);
342
- }
343
-
344
- .cpub-prop-label {
345
- display: block;
346
- font-size: var(--text-xs);
347
- font-weight: var(--font-weight-medium);
348
- color: var(--text-dim);
349
- margin-bottom: var(--space-1);
350
- }
351
-
352
- .cpub-prop-input,
353
- .cpub-prop-select,
354
- .cpub-prop-textarea {
355
- width: 100%;
356
- padding: var(--space-1) var(--space-2);
357
- background: var(--surface2);
358
- border: var(--border-width-default) solid var(--border2);
359
- color: var(--text);
360
- font-family: var(--font-sans);
361
- font-size: var(--text-sm);
362
- }
363
-
364
- .cpub-prop-input:focus,
365
- .cpub-prop-select:focus,
366
- .cpub-prop-textarea:focus {
367
- outline: none;
368
- border-color: var(--accent);
369
- }
370
-
371
- .cpub-prop-textarea {
372
- resize: vertical;
373
- }
374
-
375
- .cpub-prop-select {
376
- appearance: none;
377
- cursor: pointer;
378
- }
379
-
380
- .cpub-prop-hint {
381
- display: block;
382
- font-size: 0.6875rem;
383
- color: var(--text-faint);
384
- margin-top: 4px;
385
- line-height: 1.4;
386
- }
387
-
388
- .cpub-prop-value {
389
- font-size: var(--text-sm);
390
- color: var(--text);
391
- text-transform: capitalize;
392
- }
393
- </style>