@commonpub/layer 0.9.0 → 0.9.2
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.
|
@@ -18,7 +18,7 @@ onMounted(() => {
|
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
20
|
<template>
|
|
21
|
-
<div class="cpub-block-checkpoint" :class="{ visible: completed }">
|
|
21
|
+
<div class="cpub-block-checkpoint" :class="{ visible: completed }" role="status" aria-live="polite" aria-atomic="true">
|
|
22
22
|
<i class="fa-solid fa-circle-check"></i>
|
|
23
23
|
<span class="cpub-checkpoint-text">{{ label }}</span>
|
|
24
24
|
</div>
|
|
@@ -77,7 +77,7 @@ function optionClass(idx: number): string {
|
|
|
77
77
|
</button>
|
|
78
78
|
</div>
|
|
79
79
|
|
|
80
|
-
<div v-if="answered" class="cpub-quiz-feedback" :class="isCorrect ? 'correct' : 'wrong'">
|
|
80
|
+
<div v-if="answered" class="cpub-quiz-feedback" :class="isCorrect ? 'correct' : 'wrong'" role="status" aria-live="polite" aria-atomic="true">
|
|
81
81
|
<i :class="isCorrect ? 'fa-solid fa-circle-check' : 'fa-solid fa-circle-xmark'"></i>
|
|
82
82
|
<span>{{ isCorrect ? 'Correct!' : 'Not quite — the correct answer is highlighted above.' }}</span>
|
|
83
83
|
</div>
|
|
@@ -147,6 +147,14 @@ watch(activeSection, () => {
|
|
|
147
147
|
// Scroll section viewport to top on section change
|
|
148
148
|
const viewport = document.querySelector('.cpub-section-viewport');
|
|
149
149
|
if (viewport) viewport.scrollTop = 0;
|
|
150
|
+
// Focus section heading for screen reader announcement
|
|
151
|
+
nextTick(() => {
|
|
152
|
+
const heading = viewport?.querySelector('h1, h2, h3') as HTMLElement | null;
|
|
153
|
+
if (heading) {
|
|
154
|
+
heading.setAttribute('tabindex', '-1');
|
|
155
|
+
heading.focus();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
150
158
|
});
|
|
151
159
|
|
|
152
160
|
// Current section data
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@aws-sdk/client-s3": "^3.1010.0",
|
|
31
31
|
"@commonpub/explainer": "^0.7.11",
|
|
32
|
-
"@commonpub/schema": "^0.9.
|
|
33
|
-
"@commonpub/server": "^2.
|
|
32
|
+
"@commonpub/schema": "^0.9.13",
|
|
33
|
+
"@commonpub/server": "^2.31.0",
|
|
34
34
|
"@tiptap/core": "^2.11.0",
|
|
35
35
|
"@tiptap/extension-bold": "^2.11.0",
|
|
36
36
|
"@tiptap/extension-bullet-list": "^2.11.0",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"vue": "^3.4.0",
|
|
54
54
|
"vue-router": "^4.3.0",
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
|
-
"@commonpub/config": "0.9.1",
|
|
57
56
|
"@commonpub/auth": "0.5.1",
|
|
57
|
+
"@commonpub/config": "0.9.1",
|
|
58
58
|
"@commonpub/docs": "0.6.2",
|
|
59
59
|
"@commonpub/editor": "0.7.9",
|
|
60
60
|
"@commonpub/learning": "0.5.0",
|
|
@@ -4,41 +4,39 @@ definePageMeta({ middleware: 'auth' });
|
|
|
4
4
|
const { show: toast } = useToast();
|
|
5
5
|
const saving = ref(false);
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
emailDigest: false,
|
|
13
|
-
});
|
|
7
|
+
const likes = ref(true);
|
|
8
|
+
const comments = ref(true);
|
|
9
|
+
const follows = ref(true);
|
|
10
|
+
const mentions = ref(true);
|
|
11
|
+
const digest = ref<'none' | 'daily' | 'weekly'>('none');
|
|
14
12
|
|
|
15
13
|
// Load current preferences from profile
|
|
16
14
|
import type { Serialized, UserProfile } from '@commonpub/server';
|
|
17
15
|
|
|
18
|
-
const { data: profile, pending } = await useFetch<Serialized<UserProfile> & {
|
|
19
|
-
if (profile.value?.
|
|
20
|
-
const saved = profile.value.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
const { data: profile, pending } = await useFetch<Serialized<UserProfile> & { emailNotifications?: { digest?: string; likes?: boolean; comments?: boolean; follows?: boolean; mentions?: boolean } }>('/api/profile');
|
|
17
|
+
if (profile.value?.emailNotifications) {
|
|
18
|
+
const saved = profile.value.emailNotifications;
|
|
19
|
+
if (saved.likes !== undefined) likes.value = saved.likes;
|
|
20
|
+
if (saved.comments !== undefined) comments.value = saved.comments;
|
|
21
|
+
if (saved.follows !== undefined) follows.value = saved.follows;
|
|
22
|
+
if (saved.mentions !== undefined) mentions.value = saved.mentions;
|
|
23
|
+
if (saved.digest) digest.value = saved.digest as 'none' | 'daily' | 'weekly';
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
const labels: Record<string, string> = {
|
|
29
|
-
emailLikes: 'Email when someone likes your content',
|
|
30
|
-
emailComments: 'Email when someone comments on your content',
|
|
31
|
-
emailFollows: 'Email when someone follows you',
|
|
32
|
-
emailMentions: 'Email when someone mentions you',
|
|
33
|
-
emailDigest: 'Weekly digest email',
|
|
34
|
-
};
|
|
35
|
-
|
|
36
26
|
async function handleSave(): Promise<void> {
|
|
37
27
|
saving.value = true;
|
|
38
28
|
try {
|
|
39
29
|
await $fetch('/api/profile', {
|
|
40
30
|
method: 'PUT',
|
|
41
|
-
body: {
|
|
31
|
+
body: {
|
|
32
|
+
emailNotifications: {
|
|
33
|
+
likes: likes.value,
|
|
34
|
+
comments: comments.value,
|
|
35
|
+
follows: follows.value,
|
|
36
|
+
mentions: mentions.value,
|
|
37
|
+
digest: digest.value,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
42
40
|
});
|
|
43
41
|
toast('Preferences saved', 'success');
|
|
44
42
|
} catch (err: unknown) {
|
|
@@ -59,11 +57,32 @@ async function handleSave(): Promise<void> {
|
|
|
59
57
|
</div>
|
|
60
58
|
|
|
61
59
|
<template v-else>
|
|
62
|
-
<div
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</
|
|
60
|
+
<div class="cpub-prefs-section">
|
|
61
|
+
<h3 class="cpub-section-subtitle">Email Notifications</h3>
|
|
62
|
+
<div class="cpub-pref-row">
|
|
63
|
+
<label class="cpub-checkbox"><input type="checkbox" v-model="likes" /> Email when someone likes your content</label>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="cpub-pref-row">
|
|
66
|
+
<label class="cpub-checkbox"><input type="checkbox" v-model="comments" /> Email when someone comments on your content</label>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="cpub-pref-row">
|
|
69
|
+
<label class="cpub-checkbox"><input type="checkbox" v-model="follows" /> Email when someone follows you</label>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="cpub-pref-row">
|
|
72
|
+
<label class="cpub-checkbox"><input type="checkbox" v-model="mentions" /> Email when someone mentions you</label>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="cpub-prefs-section">
|
|
77
|
+
<h3 class="cpub-section-subtitle">Digest</h3>
|
|
78
|
+
<div class="cpub-pref-row">
|
|
79
|
+
<label for="digest-select">Summary email frequency</label>
|
|
80
|
+
<select id="digest-select" v-model="digest" class="cpub-input" style="max-width: 200px;">
|
|
81
|
+
<option value="none">None</option>
|
|
82
|
+
<option value="daily">Daily (8am UTC)</option>
|
|
83
|
+
<option value="weekly">Weekly (Monday 8am UTC)</option>
|
|
84
|
+
</select>
|
|
85
|
+
</div>
|
|
67
86
|
</div>
|
|
68
87
|
|
|
69
88
|
<button class="cpub-btn cpub-btn-primary cpub-btn-sm" style="margin-top: 16px" :disabled="saving" @click="handleSave">
|
|
@@ -72,3 +91,9 @@ async function handleSave(): Promise<void> {
|
|
|
72
91
|
</template>
|
|
73
92
|
</div>
|
|
74
93
|
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
.cpub-prefs-section { margin-bottom: 24px; }
|
|
97
|
+
.cpub-section-subtitle { font-size: 13px; font-weight: 700; margin-bottom: 12px; color: var(--text); }
|
|
98
|
+
.cpub-pref-row { margin-bottom: 10px; }
|
|
99
|
+
</style>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { indexContent, configureContentIndex } from '@commonpub/server';
|
|
2
|
+
import { contentItems } from '@commonpub/schema';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
import type { MeiliClient } from '@commonpub/server';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rebuild the entire Meilisearch content index.
|
|
8
|
+
* Iterates all published content and re-indexes each item.
|
|
9
|
+
* Requires admin role. Rate-limited to prevent abuse.
|
|
10
|
+
*/
|
|
11
|
+
export default defineEventHandler(async (event) => {
|
|
12
|
+
const user = requireAuth(event);
|
|
13
|
+
requireAdmin(event);
|
|
14
|
+
|
|
15
|
+
const meiliUrl = process.env.MEILI_URL;
|
|
16
|
+
const meiliKey = process.env.MEILI_MASTER_KEY;
|
|
17
|
+
if (!meiliUrl) {
|
|
18
|
+
throw createError({ statusCode: 400, statusMessage: 'Meilisearch not configured' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const db = useDB();
|
|
22
|
+
|
|
23
|
+
let client: MeiliClient;
|
|
24
|
+
try {
|
|
25
|
+
const { MeiliSearch } = await import('meilisearch');
|
|
26
|
+
client = new MeiliSearch({ host: meiliUrl, apiKey: meiliKey }) as unknown as MeiliClient;
|
|
27
|
+
await configureContentIndex(client);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
throw createError({ statusCode: 503, statusMessage: 'Failed to connect to Meilisearch' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fetch all published content IDs
|
|
33
|
+
const published = await db
|
|
34
|
+
.select({ id: contentItems.id })
|
|
35
|
+
.from(contentItems)
|
|
36
|
+
.where(eq(contentItems.status, 'published'));
|
|
37
|
+
|
|
38
|
+
let indexed = 0;
|
|
39
|
+
let errors = 0;
|
|
40
|
+
|
|
41
|
+
for (const item of published) {
|
|
42
|
+
try {
|
|
43
|
+
await indexContent(db, item.id, client);
|
|
44
|
+
indexed++;
|
|
45
|
+
} catch {
|
|
46
|
+
errors++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
indexed,
|
|
53
|
+
errors,
|
|
54
|
+
total: published.length,
|
|
55
|
+
};
|
|
56
|
+
});
|