@commonpub/layer 0.13.0 → 0.14.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.
- package/components/DiscussionItem.vue +4 -1
- package/components/PollDisplay.vue +1 -1
- package/components/hub/HubDiscussions.vue +2 -0
- package/components/nav/MobileNavRenderer.vue +1 -1
- package/components/nav/NavDropdown.vue +1 -1
- package/components/nav/NavLink.vue +2 -2
- package/composables/useAuth.ts +20 -15
- package/composables/useFeatures.ts +33 -14
- package/layouts/default.vue +29 -23
- package/middleware/feature-gate.global.ts +8 -4
- package/package.json +6 -6
- package/pages/contests/[slug]/index.vue +1 -0
- package/pages/events/[slug]/index.vue +1 -1
- package/pages/federated-hubs/[id]/index.vue +1 -0
- package/pages/hubs/[slug]/index.vue +1 -0
- package/server/api/contests/[slug]/judge.post.ts +1 -1
- package/types/hub.ts +1 -0
|
@@ -5,10 +5,13 @@ const props = defineProps<{
|
|
|
5
5
|
authorAvatarUrl?: string | null;
|
|
6
6
|
replyCount: number;
|
|
7
7
|
voteCount: number;
|
|
8
|
+
voteScore?: number;
|
|
8
9
|
lastReplyAt?: Date;
|
|
9
10
|
solved?: boolean;
|
|
10
11
|
}>();
|
|
11
12
|
|
|
13
|
+
const displayScore = computed(() => props.voteScore ?? props.voteCount);
|
|
14
|
+
|
|
12
15
|
const emit = defineEmits<{
|
|
13
16
|
upvote: [];
|
|
14
17
|
downvote: [];
|
|
@@ -33,7 +36,7 @@ const lastReplyFormatted = computed((): string | null => {
|
|
|
33
36
|
>
|
|
34
37
|
<i class="fa-solid fa-chevron-up"></i>
|
|
35
38
|
</button>
|
|
36
|
-
<span class="cpub-vote-count">{{
|
|
39
|
+
<span class="cpub-vote-count">{{ displayScore }}</span>
|
|
37
40
|
<button
|
|
38
41
|
class="cpub-vote-btn"
|
|
39
42
|
aria-label="Downvote"
|
|
@@ -16,7 +16,7 @@ const { data, refresh } = await useFetch<{ options: PollOptionResult[]; userVote
|
|
|
16
16
|
|
|
17
17
|
const totalVotes = computed(() => {
|
|
18
18
|
if (!data.value) return 0;
|
|
19
|
-
return data.value.options.reduce((sum, opt) => sum + opt.voteCount, 0);
|
|
19
|
+
return data.value.options.reduce((sum: number, opt: { voteCount: number }) => sum + opt.voteCount, 0);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
const hasVoted = computed(() => !!data.value?.userVote);
|
|
@@ -29,6 +29,7 @@ const discussionPosts = computed(() => {
|
|
|
29
29
|
:author="post.author.name"
|
|
30
30
|
:reply-count="post.replyCount"
|
|
31
31
|
:vote-count="post.likeCount"
|
|
32
|
+
:vote-score="post.voteScore"
|
|
32
33
|
/>
|
|
33
34
|
</NuxtLink>
|
|
34
35
|
<div v-else>
|
|
@@ -37,6 +38,7 @@ const discussionPosts = computed(() => {
|
|
|
37
38
|
:author="post.author.name"
|
|
38
39
|
:reply-count="post.replyCount"
|
|
39
40
|
:vote-count="post.likeCount"
|
|
41
|
+
:vote-score="post.voteScore"
|
|
40
42
|
/>
|
|
41
43
|
</div>
|
|
42
44
|
</template>
|
|
@@ -31,7 +31,7 @@ function isChildVisible(child: NavItem): boolean {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const visibleChildren = computed(() =>
|
|
34
|
-
(props.item.children ?? []).filter(c => isChildVisible(c)),
|
|
34
|
+
(props.item.children ?? []).filter((c: NavItem) => isChildVisible(c)),
|
|
35
35
|
);
|
|
36
36
|
|
|
37
37
|
function handleKeydown(e: KeyboardEvent): void {
|
|
@@ -9,7 +9,7 @@ const isExternal = computed(() => props.item.type === 'external' && props.item.h
|
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<template>
|
|
12
|
-
<span v-if="item.disabled" class="cpub-nav-link cpub-nav-
|
|
12
|
+
<span v-if="item.disabled" class="cpub-nav-link cpub-nav-link--disabled">
|
|
13
13
|
<i v-if="item.icon" :class="item.icon"></i> {{ item.label }}
|
|
14
14
|
</span>
|
|
15
15
|
<a
|
|
@@ -34,7 +34,7 @@ const isExternal = computed(() => props.item.type === 'external' && props.item.h
|
|
|
34
34
|
<style scoped>
|
|
35
35
|
.cpub-nav-external-icon {
|
|
36
36
|
font-size: 8px;
|
|
37
|
-
|
|
37
|
+
color: var(--text-faint);
|
|
38
38
|
margin-left: 2px;
|
|
39
39
|
}
|
|
40
40
|
</style>
|
package/composables/useAuth.ts
CHANGED
|
@@ -20,6 +20,21 @@ export interface ClientAuthSession {
|
|
|
20
20
|
expiresAt: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
interface AuthResponse {
|
|
24
|
+
user: ClientAuthUser | null;
|
|
25
|
+
session: ClientAuthSession | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Type-safe POST fetch that avoids Nuxt $fetch TS2589 deep instantiation */
|
|
29
|
+
async function authPost(url: string, body: Record<string, unknown>): Promise<AuthResponse | null> {
|
|
30
|
+
return ($fetch as (url: string, opts: Record<string, unknown>) => Promise<AuthResponse | null>)(url, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
body,
|
|
33
|
+
credentials: 'include',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
export function useAuth() {
|
|
24
39
|
const user = useState<ClientAuthUser | null>('auth-user', () => null);
|
|
25
40
|
const session = useState<ClientAuthSession | null>('auth-session', () => null);
|
|
@@ -28,27 +43,19 @@ export function useAuth() {
|
|
|
28
43
|
const isAdmin = computed(() => user.value?.role === 'admin');
|
|
29
44
|
|
|
30
45
|
async function signIn(email: string, password: string): Promise<void> {
|
|
31
|
-
const data = await
|
|
32
|
-
method: 'POST',
|
|
33
|
-
body: { email, password },
|
|
34
|
-
credentials: 'include',
|
|
35
|
-
});
|
|
46
|
+
const data = await authPost('/api/auth/sign-in/email', { email, password });
|
|
36
47
|
user.value = data?.user ?? null;
|
|
37
48
|
session.value = data?.session ?? null;
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
async function signUp(email: string, password: string, username: string): Promise<void> {
|
|
41
|
-
const data = await
|
|
42
|
-
method: 'POST',
|
|
43
|
-
body: { email, password, username, name: username },
|
|
44
|
-
credentials: 'include',
|
|
45
|
-
});
|
|
52
|
+
const data = await authPost('/api/auth/sign-up/email', { email, password, username, name: username });
|
|
46
53
|
user.value = data?.user ?? null;
|
|
47
54
|
session.value = data?.session ?? null;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
async function signOut(): Promise<void> {
|
|
51
|
-
await
|
|
58
|
+
await authPost('/api/auth/sign-out', {});
|
|
52
59
|
user.value = null;
|
|
53
60
|
session.value = null;
|
|
54
61
|
await navigateTo('/');
|
|
@@ -61,14 +68,12 @@ export function useAuth() {
|
|
|
61
68
|
async function refreshSession(): Promise<void> {
|
|
62
69
|
if (import.meta.server) return;
|
|
63
70
|
try {
|
|
64
|
-
const data = await $fetch
|
|
65
|
-
'/api/me',
|
|
66
|
-
{ credentials: 'include' },
|
|
71
|
+
const data = await ($fetch as (url: string, opts: Record<string, unknown>) => Promise<AuthResponse | null>)(
|
|
72
|
+
'/api/me', { credentials: 'include' },
|
|
67
73
|
);
|
|
68
74
|
user.value = data?.user ?? null;
|
|
69
75
|
session.value = data?.session ?? null;
|
|
70
76
|
} catch {
|
|
71
|
-
// Session invalid or server unreachable — clear client state
|
|
72
77
|
user.value = null;
|
|
73
78
|
session.value = null;
|
|
74
79
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// Feature flag composable — reactive access to enabled features
|
|
2
|
+
// Initializes from build-time runtime config, then hydrates from /api/features
|
|
3
|
+
// to pick up runtime DB overrides set via admin panel.
|
|
2
4
|
|
|
3
5
|
export interface FeatureFlags {
|
|
4
6
|
content: boolean;
|
|
@@ -16,24 +18,41 @@ export interface FeatureFlags {
|
|
|
16
18
|
emailNotifications: boolean;
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
let hydrated = false;
|
|
22
|
+
|
|
19
23
|
export function useFeatures() {
|
|
20
24
|
const config = useRuntimeConfig();
|
|
21
|
-
const
|
|
25
|
+
const buildFlags = config.public.features as unknown as FeatureFlags;
|
|
26
|
+
|
|
27
|
+
// Shared reactive state — initialized from build-time config
|
|
28
|
+
const flags = useState<FeatureFlags>('feature-flags', () => ({ ...buildFlags }));
|
|
29
|
+
|
|
30
|
+
// On client, fetch dynamic features once to pick up DB overrides
|
|
31
|
+
if (import.meta.client && !hydrated) {
|
|
32
|
+
hydrated = true;
|
|
33
|
+
($fetch as Function)('/api/features')
|
|
34
|
+
.then((dynamic: FeatureFlags) => {
|
|
35
|
+
if (dynamic && typeof dynamic === 'object') {
|
|
36
|
+
flags.value = { ...flags.value, ...dynamic };
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.catch(() => { /* use build-time defaults on failure */ });
|
|
40
|
+
}
|
|
22
41
|
|
|
23
42
|
return {
|
|
24
43
|
features: flags,
|
|
25
|
-
content: computed(() => flags.content),
|
|
26
|
-
social: computed(() => flags.social),
|
|
27
|
-
hubs: computed(() => flags.hubs),
|
|
28
|
-
docs: computed(() => flags.docs),
|
|
29
|
-
video: computed(() => flags.video),
|
|
30
|
-
contests: computed(() => flags.contests),
|
|
31
|
-
events: computed(() => flags.events),
|
|
32
|
-
learning: computed(() => flags.learning),
|
|
33
|
-
explainers: computed(() => flags.explainers),
|
|
34
|
-
editorial: computed(() => flags.editorial),
|
|
35
|
-
federation: computed(() => flags.federation),
|
|
36
|
-
admin: computed(() => flags.admin),
|
|
37
|
-
emailNotifications: computed(() => flags.emailNotifications),
|
|
44
|
+
content: computed(() => flags.value.content),
|
|
45
|
+
social: computed(() => flags.value.social),
|
|
46
|
+
hubs: computed(() => flags.value.hubs),
|
|
47
|
+
docs: computed(() => flags.value.docs),
|
|
48
|
+
video: computed(() => flags.value.video),
|
|
49
|
+
contests: computed(() => flags.value.contests),
|
|
50
|
+
events: computed(() => flags.value.events),
|
|
51
|
+
learning: computed(() => flags.value.learning),
|
|
52
|
+
explainers: computed(() => flags.value.explainers),
|
|
53
|
+
editorial: computed(() => flags.value.editorial),
|
|
54
|
+
federation: computed(() => flags.value.federation),
|
|
55
|
+
admin: computed(() => flags.value.admin),
|
|
56
|
+
emailNotifications: computed(() => flags.value.emailNotifications),
|
|
38
57
|
};
|
|
39
58
|
}
|
package/layouts/default.vue
CHANGED
|
@@ -21,7 +21,11 @@ const mobileMenuOpen = ref(false);
|
|
|
21
21
|
const openDropdown = ref<string | null>(null);
|
|
22
22
|
|
|
23
23
|
// Fetch configurable nav items (falls back to defaults on server)
|
|
24
|
-
|
|
24
|
+
// useAsyncData avoids Nuxt's typed route inference which triggers TS2589
|
|
25
|
+
const { data: navItems } = await useAsyncData('nav-items', () =>
|
|
26
|
+
($fetch as Function)('/api/navigation/items') as Promise<NavItem[]>,
|
|
27
|
+
{ default: () => [] as NavItem[] },
|
|
28
|
+
);
|
|
25
29
|
|
|
26
30
|
function toggleDropdown(name: string): void {
|
|
27
31
|
openDropdown.value = openDropdown.value === name ? null : name;
|
|
@@ -224,42 +228,44 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
224
228
|
}
|
|
225
229
|
.cpub-topbar-logo { display: flex; align-items: center; flex-shrink: 0; text-decoration: none; color: var(--text); }
|
|
226
230
|
|
|
227
|
-
|
|
228
|
-
.cpub-nav
|
|
229
|
-
.cpub-nav-link
|
|
230
|
-
.cpub-nav-link
|
|
231
|
-
.cpub-nav-link
|
|
231
|
+
/* Nav styles use :deep() to reach into NavRenderer/NavDropdown/NavLink child components */
|
|
232
|
+
:deep(.cpub-topbar-nav) { display: flex; align-items: center; gap: 2px; margin-left: 24px; }
|
|
233
|
+
:deep(.cpub-nav-link) { font-size: 12px; color: var(--text-dim); padding: 5px 12px; border: var(--border-width-default) solid transparent; background: none; text-decoration: none; transition: color 0.15s, background 0.15s; display: flex; align-items: center; gap: 6px; }
|
|
234
|
+
:deep(.cpub-nav-link i) { font-size: 10px; }
|
|
235
|
+
:deep(.cpub-nav-link:hover) { color: var(--text); background: var(--surface2); }
|
|
236
|
+
:deep(.cpub-nav-link.router-link-active) { color: var(--text); background: var(--surface2); border-color: var(--border); }
|
|
237
|
+
:deep(.cpub-nav-link--disabled) { opacity: 0.35; cursor: not-allowed; pointer-events: none; }
|
|
232
238
|
|
|
233
239
|
/* Nav dropdowns */
|
|
234
|
-
.cpub-nav-dropdown { position: relative; }
|
|
235
|
-
.cpub-nav-trigger { cursor: pointer; }
|
|
236
|
-
.cpub-nav-caret { font-size: 7px !important; margin-left: 2px; transition: transform 0.15s; }
|
|
237
|
-
.cpub-nav-trigger--open .cpub-nav-caret { transform: rotate(180deg); }
|
|
238
|
-
.cpub-nav-panel {
|
|
240
|
+
:deep(.cpub-nav-dropdown) { position: relative; }
|
|
241
|
+
:deep(.cpub-nav-trigger) { cursor: pointer; }
|
|
242
|
+
:deep(.cpub-nav-caret) { font-size: 7px !important; margin-left: 2px; transition: transform 0.15s; }
|
|
243
|
+
:deep(.cpub-nav-trigger--open .cpub-nav-caret) { transform: rotate(180deg); }
|
|
244
|
+
:deep(.cpub-nav-panel) {
|
|
239
245
|
position: absolute; top: 100%; left: 0; min-width: 180px;
|
|
240
246
|
background: var(--surface); border: var(--border-width-default) solid var(--border);
|
|
241
247
|
box-shadow: var(--shadow-md); z-index: 200; display: flex; flex-direction: column; padding: 4px 0;
|
|
242
248
|
margin-top: 4px;
|
|
243
249
|
}
|
|
244
|
-
.cpub-nav-panel-item {
|
|
250
|
+
:deep(.cpub-nav-panel-item) {
|
|
245
251
|
display: flex; align-items: center; gap: 8px; padding: 8px 14px;
|
|
246
252
|
font-size: 12px; color: var(--text-dim); text-decoration: none;
|
|
247
253
|
transition: background 0.1s, color 0.1s; cursor: pointer;
|
|
248
254
|
}
|
|
249
|
-
.cpub-nav-panel-item:hover { background: var(--surface2); color: var(--text); }
|
|
250
|
-
.cpub-nav-panel-item i { width: 14px; text-align: center; font-size: 11px; }
|
|
251
|
-
.cpub-nav-panel-item--disabled {
|
|
255
|
+
:deep(.cpub-nav-panel-item:hover) { background: var(--surface2); color: var(--text); }
|
|
256
|
+
:deep(.cpub-nav-panel-item i) { width: 14px; text-align: center; font-size: 11px; }
|
|
257
|
+
:deep(.cpub-nav-panel-item--disabled) {
|
|
252
258
|
opacity: 0.35; cursor: not-allowed; pointer-events: none;
|
|
253
259
|
}
|
|
254
260
|
|
|
255
|
-
/* Mobile nav sections */
|
|
256
|
-
.cpub-mobile-section-label {
|
|
261
|
+
/* Mobile nav sections — :deep() for MobileNavRenderer child component */
|
|
262
|
+
:deep(.cpub-mobile-section-label) {
|
|
257
263
|
font-family: var(--font-mono); font-size: 9px; font-weight: 700;
|
|
258
264
|
text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-faint);
|
|
259
265
|
padding: 10px 20px 2px; margin-top: 4px;
|
|
260
266
|
}
|
|
261
|
-
.cpub-mobile-link--indent { padding-left: 36px; }
|
|
262
|
-
.cpub-mobile-link--disabled { opacity: 0.35; cursor: not-allowed; pointer-events: none; }
|
|
267
|
+
:deep(.cpub-mobile-link--indent) { padding-left: 36px; }
|
|
268
|
+
:deep(.cpub-mobile-link--disabled) { opacity: 0.35; cursor: not-allowed; pointer-events: none; }
|
|
263
269
|
|
|
264
270
|
.cpub-topbar-spacer { flex: 1; }
|
|
265
271
|
.cpub-topbar-actions { display: flex; align-items: center; gap: 6px; }
|
|
@@ -288,10 +294,10 @@ const userUsername = computed(() => user.value?.username ?? '');
|
|
|
288
294
|
|
|
289
295
|
.cpub-mobile-toggle { display: none; width: 32px; height: 32px; background: none; border: var(--border-width-default) solid transparent; color: var(--text-dim); font-size: 16px; cursor: pointer; align-items: center; justify-content: center; }
|
|
290
296
|
.cpub-mobile-menu { display: none; position: fixed; inset: 0; top: 48px; z-index: 99; background: var(--color-surface-overlay-light); }
|
|
291
|
-
.cpub-mobile-nav { background: var(--surface); border-bottom: var(--border-width-default) solid var(--border); padding: 8px 0; display: flex; flex-direction: column; box-shadow: var(--shadow-md); }
|
|
292
|
-
.cpub-mobile-link { display: flex; align-items: center; gap: 10px; padding: 10px 20px; font-size: 13px; color: var(--text-dim); text-decoration: none; transition: background 0.1s; }
|
|
293
|
-
.cpub-mobile-link:hover { background: var(--surface2); color: var(--text); }
|
|
294
|
-
.cpub-mobile-link i { width: 16px; text-align: center; font-size: 12px; }
|
|
297
|
+
:deep(.cpub-mobile-nav) { background: var(--surface); border-bottom: var(--border-width-default) solid var(--border); padding: 8px 0; display: flex; flex-direction: column; box-shadow: var(--shadow-md); }
|
|
298
|
+
:deep(.cpub-mobile-link) { display: flex; align-items: center; gap: 10px; padding: 10px 20px; font-size: 13px; color: var(--text-dim); text-decoration: none; transition: background 0.1s; }
|
|
299
|
+
:deep(.cpub-mobile-link:hover) { background: var(--surface2); color: var(--text); }
|
|
300
|
+
:deep(.cpub-mobile-link i) { width: 16px; text-align: center; font-size: 12px; }
|
|
295
301
|
.cpub-mobile-divider { height: 2px; background: var(--border2); margin: 4px 16px; }
|
|
296
302
|
.cpub-mobile-nav-extra { border-top: var(--border-width-default) solid var(--border2); }
|
|
297
303
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Global client-side middleware that mirrors server/middleware/features.ts.
|
|
2
2
|
// Prevents client-side navigation to feature-gated pages when the flag is disabled.
|
|
3
|
+
// Uses useState('feature-flags') which is hydrated by useFeatures() from /api/features.
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
import type { FeatureFlags } from '../composables/useFeatures';
|
|
6
|
+
|
|
7
|
+
const ROUTE_FEATURE_MAP: Record<string, keyof FeatureFlags> = {
|
|
5
8
|
'/learn': 'learning',
|
|
6
9
|
'/docs': 'docs',
|
|
7
10
|
'/videos': 'video',
|
|
@@ -14,9 +17,10 @@ const ROUTE_FEATURE_MAP: Record<string, keyof import('../composables/useFeatures
|
|
|
14
17
|
export default defineNuxtRouteMiddleware((to) => {
|
|
15
18
|
for (const [prefix, feature] of Object.entries(ROUTE_FEATURE_MAP)) {
|
|
16
19
|
if (to.path === prefix || to.path.startsWith(prefix + '/')) {
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
+
// Prefer reactive state (hydrated from /api/features), fall back to build-time config
|
|
21
|
+
const featureState = useState<FeatureFlags | null>('feature-flags', () => null);
|
|
22
|
+
const flags = featureState.value ?? (useRuntimeConfig().public.features as Record<string, boolean>);
|
|
23
|
+
if (!(flags as Record<string, boolean>)[feature]) {
|
|
20
24
|
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
21
25
|
}
|
|
22
26
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@aws-sdk/client-s3": "^3.1010.0",
|
|
31
31
|
"@commonpub/explainer": "^0.7.11",
|
|
32
32
|
"@commonpub/schema": "^0.13.0",
|
|
33
|
-
"@commonpub/server": "^2.
|
|
33
|
+
"@commonpub/server": "^2.41.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,13 +53,13 @@
|
|
|
53
53
|
"vue": "^3.4.0",
|
|
54
54
|
"vue-router": "^4.3.0",
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
|
-
"@commonpub/
|
|
56
|
+
"@commonpub/auth": "0.5.1",
|
|
57
57
|
"@commonpub/editor": "0.7.9",
|
|
58
|
+
"@commonpub/docs": "0.6.2",
|
|
58
59
|
"@commonpub/learning": "0.5.0",
|
|
60
|
+
"@commonpub/protocol": "0.9.9",
|
|
59
61
|
"@commonpub/config": "0.10.0",
|
|
60
|
-
"@commonpub/
|
|
61
|
-
"@commonpub/ui": "0.8.5",
|
|
62
|
-
"@commonpub/protocol": "0.9.9"
|
|
62
|
+
"@commonpub/ui": "0.8.5"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -140,6 +140,7 @@ async function withdrawEntry(entryId: string): Promise<void> {
|
|
|
140
140
|
<ContestRules v-if="c?.rules" :rules="c.rules" />
|
|
141
141
|
<ContestPrizes v-if="c?.prizes?.length" :prizes="c.prizes" />
|
|
142
142
|
<ContestJudges v-if="c?.judges?.length" :judge-ids="c.judges" />
|
|
143
|
+
<ContestJudgeManager v-if="isOwner && c" :contest-slug="slug" :is-owner="isOwner" />
|
|
143
144
|
<ContestEntries
|
|
144
145
|
:entries="entries"
|
|
145
146
|
:contest-status="c?.status"
|
|
@@ -15,7 +15,7 @@ const rsvpLoading = ref(false);
|
|
|
15
15
|
|
|
16
16
|
const myRsvpStatus = computed((): AttendeeStatus | null => {
|
|
17
17
|
if (!isAuthenticated.value || !attendees.value) return null;
|
|
18
|
-
const found = attendees.value.items.find(a => a.userId === user.value?.id);
|
|
18
|
+
const found = attendees.value.items.find((a: AttendeeItem) => a.userId === user.value?.id);
|
|
19
19
|
return found?.status ?? null;
|
|
20
20
|
});
|
|
21
21
|
|
|
@@ -74,6 +74,7 @@ const postsVM = computed<HubPostViewModel[]>(() => {
|
|
|
74
74
|
},
|
|
75
75
|
createdAt: String(p.publishedAt ?? p.receivedAt),
|
|
76
76
|
likeCount: (p.localLikeCount ?? 0) + (p.remoteLikeCount ?? 0),
|
|
77
|
+
voteScore: 0,
|
|
77
78
|
replyCount: (p.localReplyCount ?? 0) + (p.remoteReplyCount ?? 0),
|
|
78
79
|
isPinned: p.isPinned ?? false,
|
|
79
80
|
isLocked: false,
|
|
@@ -9,7 +9,7 @@ export default defineEventHandler(async (event): Promise<{ success: boolean }> =
|
|
|
9
9
|
|
|
10
10
|
const result = await judgeContestEntry(db, input.entryId, input.score, user.id, input.feedback);
|
|
11
11
|
if (!result.judged) {
|
|
12
|
-
throw createError({ statusCode: 403,
|
|
12
|
+
throw createError({ statusCode: 403, statusMessage: result.error ?? 'Judging failed' });
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
return { success: true };
|