@commonpub/layer 0.23.3 → 0.24.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/sections/SectionContests.vue +193 -0
- package/components/sections/SectionCustomHtml.vue +70 -0
- package/components/sections/SectionEditorial.vue +138 -0
- package/components/sections/SectionHubs.vue +247 -0
- package/components/sections/SectionLearning.vue +232 -0
- package/components/sections/SectionStats.vue +151 -0
- package/composables/useFeatures.ts +32 -5
- package/composables/useLayout.ts +12 -2
- package/nuxt.config.ts +14 -0
- package/package.json +6 -6
- package/sections/builtin/contests.ts +38 -0
- package/sections/builtin/custom-html.ts +50 -0
- package/sections/builtin/editorial.ts +39 -0
- package/sections/builtin/hubs.ts +45 -0
- package/sections/builtin/learning.ts +38 -0
- package/sections/builtin/stats.ts +36 -0
- package/sections/registry.ts +27 -7
- package/server/api/admin/layouts/migrate-homepage.post.ts +56 -0
- package/server/plugins/feature-flags-prime.ts +39 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in section definition: hubs.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1c addition (session 159) — trending hubs list. Server-fetches
|
|
5
|
+
* `/api/hubs?limit=N` and renders a sidebar-style card with icon, name,
|
|
6
|
+
* member count, and a Join CTA per hub.
|
|
7
|
+
*
|
|
8
|
+
* Renderer also dispatches join requests via `POST /api/hubs/:slug/join`
|
|
9
|
+
* for authenticated visitors (mirrors legacy `HubsSection.vue`); guests
|
|
10
|
+
* are redirected to `/auth/login?redirect=/`.
|
|
11
|
+
*
|
|
12
|
+
* Defaults to colSpan 4 (sidebar). Set colSpan 12 for a horizontal
|
|
13
|
+
* sweep on a community-discovery page.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import type { SectionDefinition } from '@commonpub/ui';
|
|
17
|
+
import SectionHubs from '../../components/sections/SectionHubs.vue';
|
|
18
|
+
|
|
19
|
+
const configSchema = z.object({
|
|
20
|
+
heading: z.string().max(120).default('Trending Hubs'),
|
|
21
|
+
limit: z.number().int().min(1).max(20).default(4),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const hubsSection: SectionDefinition<z.infer<typeof configSchema>> = {
|
|
25
|
+
type: 'hubs',
|
|
26
|
+
name: 'Hubs',
|
|
27
|
+
description: 'Trending hubs list with join action (feature-gated)',
|
|
28
|
+
icon: 'fa-users',
|
|
29
|
+
category: 'data',
|
|
30
|
+
status: 'stable',
|
|
31
|
+
// Palette gate — admins on a hubs-disabled instance shouldn't see this
|
|
32
|
+
// type in the section-picker. Runtime gating is separate: each placed
|
|
33
|
+
// instance's `visibility.features` array controls render visibility
|
|
34
|
+
// (LayoutSlot honors it). Setting both here AND in the migration script
|
|
35
|
+
// is the belt-and-braces.
|
|
36
|
+
featureGate: 'hubs',
|
|
37
|
+
configSchema,
|
|
38
|
+
defaultConfig: { heading: 'Trending Hubs', limit: 4 },
|
|
39
|
+
schemaVersion: 1,
|
|
40
|
+
component: SectionHubs,
|
|
41
|
+
minColSpan: 3,
|
|
42
|
+
maxColSpan: 12,
|
|
43
|
+
defaultColSpan: 4,
|
|
44
|
+
resizable: true,
|
|
45
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in section definition: learning.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1c addition (session 159) — learning paths grid. Server-fetches
|
|
5
|
+
* `/api/learn?limit=N` and renders a responsive grid of cards (title,
|
|
6
|
+
* description, difficulty + duration, enrollment count).
|
|
7
|
+
*
|
|
8
|
+
* Feature-gated on `features.learning`. Behaves like editorial /
|
|
9
|
+
* content-feed structurally — a discoverable feed of an entity type.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import type { SectionDefinition } from '@commonpub/ui';
|
|
13
|
+
import SectionLearning from '../../components/sections/SectionLearning.vue';
|
|
14
|
+
|
|
15
|
+
const configSchema = z.object({
|
|
16
|
+
heading: z.string().max(120).default('Learning Paths'),
|
|
17
|
+
limit: z.number().int().min(1).max(12).default(6),
|
|
18
|
+
columns: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).default(3),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const learningSection: SectionDefinition<z.infer<typeof configSchema>> = {
|
|
22
|
+
type: 'learning',
|
|
23
|
+
name: 'Learning paths',
|
|
24
|
+
description: 'Grid of learning paths with enrollment + difficulty (feature-gated)',
|
|
25
|
+
icon: 'fa-graduation-cap',
|
|
26
|
+
category: 'data',
|
|
27
|
+
status: 'stable',
|
|
28
|
+
// Palette gate — see hubs.ts for the rationale.
|
|
29
|
+
featureGate: 'learning',
|
|
30
|
+
configSchema,
|
|
31
|
+
defaultConfig: { heading: 'Learning Paths', limit: 6, columns: 3 },
|
|
32
|
+
schemaVersion: 1,
|
|
33
|
+
component: SectionLearning,
|
|
34
|
+
minColSpan: 6,
|
|
35
|
+
maxColSpan: 12,
|
|
36
|
+
defaultColSpan: 12,
|
|
37
|
+
resizable: true,
|
|
38
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in section definition: stats.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1c addition (session 159) — platform stats grid. Server-fetches
|
|
5
|
+
* `/api/stats` (PlatformStats) and renders a small numeric grid:
|
|
6
|
+
* Projects, Posts (blog + legacy article), Members, Hubs.
|
|
7
|
+
*
|
|
8
|
+
* Hubs metric is gated on `features.hubs` — when the flag is off, the
|
|
9
|
+
* cell hides + the grid collapses to 1×3. Sidebar-friendly by default
|
|
10
|
+
* (colSpan 4), but works as a narrow row at 12.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import type { SectionDefinition } from '@commonpub/ui';
|
|
14
|
+
import SectionStats from '../../components/sections/SectionStats.vue';
|
|
15
|
+
|
|
16
|
+
const configSchema = z.object({
|
|
17
|
+
heading: z.string().max(120).default('Platform Stats'),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const statsSection: SectionDefinition<z.infer<typeof configSchema>> = {
|
|
21
|
+
type: 'stats',
|
|
22
|
+
name: 'Platform stats',
|
|
23
|
+
description: 'Numeric grid of platform totals (projects, posts, members, hubs)',
|
|
24
|
+
icon: 'fa-chart-simple',
|
|
25
|
+
category: 'data',
|
|
26
|
+
status: 'stable',
|
|
27
|
+
configSchema,
|
|
28
|
+
defaultConfig: { heading: 'Platform Stats' },
|
|
29
|
+
schemaVersion: 1,
|
|
30
|
+
component: SectionStats,
|
|
31
|
+
// Stat blocks tile down to 2×2 in a quarter-width column comfortably
|
|
32
|
+
minColSpan: 3,
|
|
33
|
+
maxColSpan: 12,
|
|
34
|
+
defaultColSpan: 4,
|
|
35
|
+
resizable: true,
|
|
36
|
+
};
|
package/sections/registry.ts
CHANGED
|
@@ -29,26 +29,46 @@ import { paragraphSection } from './builtin/paragraph';
|
|
|
29
29
|
import { imageSection } from './builtin/image';
|
|
30
30
|
import { heroSection } from './builtin/hero';
|
|
31
31
|
import { contentFeedSection } from './builtin/content-feed';
|
|
32
|
+
import { editorialSection } from './builtin/editorial';
|
|
33
|
+
import { statsSection } from './builtin/stats';
|
|
34
|
+
import { hubsSection } from './builtin/hubs';
|
|
35
|
+
import { contestsSection } from './builtin/contests';
|
|
36
|
+
import { learningSection } from './builtin/learning';
|
|
37
|
+
import { customHtmlSection } from './builtin/custom-html';
|
|
32
38
|
|
|
33
39
|
// Singleton — registered once at module load. Vue/Nuxt's setup() runs
|
|
34
40
|
// per-component, but module load is once per app process. Safe.
|
|
35
41
|
const registry = new SectionRegistry();
|
|
36
42
|
|
|
37
43
|
// --- Built-in registrations -----------------------------------------------
|
|
38
|
-
// Phase 1c
|
|
39
|
-
// the four leading categories — layout (hero,
|
|
40
|
-
// paragraph, image), and data
|
|
44
|
+
// Phase 1c full catalog (session 159): divider (proof-of-life) + 11
|
|
45
|
+
// sections covering the four leading categories — layout (hero,
|
|
46
|
+
// divider), content (heading, paragraph, image, custom-html), and data
|
|
47
|
+
// (content-feed, editorial, stats, hubs, contests, learning).
|
|
41
48
|
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
49
|
+
// With the addition of editorial / stats / hubs / contests / learning /
|
|
50
|
+
// custom-html this session, every section type the legacy
|
|
51
|
+
// `homepage.sections` JSON dispatches to (HomepageSectionRenderer.vue)
|
|
52
|
+
// is now representable as a registered section — unblocking the real
|
|
53
|
+
// legacy-homepage migration (Phase 1c step 3 in the session-158 handoff).
|
|
54
|
+
//
|
|
55
|
+
// Phase 6b will add the remaining 18 types (gallery, video, embed,
|
|
56
|
+
// spacer, cta, featured-content, content-card, contest-list, hub-list,
|
|
57
|
+
// event-list, member-list, stats-grid, contact-form, newsletter,
|
|
58
|
+
// announcement, markdown, iframe, content-grid alias). See
|
|
59
|
+
// docs/plans/layout-and-pages.md §3.4.
|
|
46
60
|
registry.register(dividerSection);
|
|
47
61
|
registry.register(heroSection);
|
|
48
62
|
registry.register(headingSection);
|
|
49
63
|
registry.register(paragraphSection);
|
|
50
64
|
registry.register(imageSection);
|
|
51
65
|
registry.register(contentFeedSection);
|
|
66
|
+
registry.register(editorialSection);
|
|
67
|
+
registry.register(statsSection);
|
|
68
|
+
registry.register(hubsSection);
|
|
69
|
+
registry.register(contestsSection);
|
|
70
|
+
registry.register(learningSection);
|
|
71
|
+
registry.register(customHtmlSection);
|
|
52
72
|
|
|
53
73
|
/**
|
|
54
74
|
* Read-only accessor — the layer's standard pattern for shared state.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/admin/layouts/migrate-homepage
|
|
3
|
+
*
|
|
4
|
+
* Converts the operator's legacy `instance_settings.homepage.sections`
|
|
5
|
+
* JSON into a real `layouts` row at scope ('route', '/').
|
|
6
|
+
*
|
|
7
|
+
* Body (all optional):
|
|
8
|
+
* { force?: boolean }
|
|
9
|
+
*
|
|
10
|
+
* Default: skip when a layout already exists at the route (returns
|
|
11
|
+
* `{migrated:false, reason:'layout-already-exists', layoutId}`). With
|
|
12
|
+
* `force: true`, the existing layout + its rows / sections / versions
|
|
13
|
+
* are deleted via FK cascade and replaced.
|
|
14
|
+
*
|
|
15
|
+
* Intended canary flow (replaces the older seed-homepage path for
|
|
16
|
+
* instances that have customised their homepage):
|
|
17
|
+
* 1. Operator runs migration 0005 (if not already applied)
|
|
18
|
+
* 2. Operator POSTs here → layouts row matching the live homepage
|
|
19
|
+
* is created + published
|
|
20
|
+
* 3. Operator flips `features.layoutEngine` ON
|
|
21
|
+
* 4. Homepage SSR renders via LayoutSlot — visually identical, but
|
|
22
|
+
* now sourced from the layouts table instead of homepage.sections
|
|
23
|
+
* 5. Once stable, the legacy `homepage.sections` setting can be
|
|
24
|
+
* removed (separate operator action — this endpoint doesn't
|
|
25
|
+
* delete legacy data)
|
|
26
|
+
*
|
|
27
|
+
* Admin + features.admin + features.layoutEngine. Invalidates the
|
|
28
|
+
* layouts-by-route cache on a successful migration (or force-replace).
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
import { migrateHomepageSectionsToLayout } from '@commonpub/server';
|
|
32
|
+
import { invalidateLayoutsByRouteCache } from '../../../utils/layoutCache';
|
|
33
|
+
|
|
34
|
+
const bodySchema = z.object({
|
|
35
|
+
force: z.boolean().optional().default(false),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export default defineEventHandler(async (event) => {
|
|
39
|
+
requireFeature('admin');
|
|
40
|
+
requireFeature('layoutEngine');
|
|
41
|
+
const admin = requireAdmin(event);
|
|
42
|
+
|
|
43
|
+
const body = await readBody(event).catch(() => ({}));
|
|
44
|
+
const { force } = bodySchema.parse(body ?? {});
|
|
45
|
+
|
|
46
|
+
const db = useDB();
|
|
47
|
+
const result = await migrateHomepageSectionsToLayout(db, {
|
|
48
|
+
adminId: admin.id,
|
|
49
|
+
force,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (result.migrated) {
|
|
53
|
+
invalidateLayoutsByRouteCache();
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nitro plugin — prime each SSR request with DB-merged feature flags.
|
|
3
|
+
*
|
|
4
|
+
* The Vue `useFeatures()` composable (layers/base/composables/useFeatures.ts)
|
|
5
|
+
* initialises its `useState('feature-flags')` from `useRuntimeConfig().public.features`,
|
|
6
|
+
* which is the BUILD-TIME config baked into the bundle. Admin-UI flag
|
|
7
|
+
* overrides land in `instance_settings.features.overrides` and are
|
|
8
|
+
* picked up by `useConfig()` (server-side, DB-merged with 60s cache),
|
|
9
|
+
* but the layer composable never queries that at SSR time — only AFTER
|
|
10
|
+
* client mount, via `$fetch('/api/features')`.
|
|
11
|
+
*
|
|
12
|
+
* Effect of the gap: SSR renders with stale (build-time) flag values.
|
|
13
|
+
* An admin flipping `layoutEngine: true` from off-by-default through the
|
|
14
|
+
* admin UI doesn't take effect for SSR; the first paint shows the
|
|
15
|
+
* v-else-if branch, then hydration replaces it with the v-if branch.
|
|
16
|
+
* Bad UX and breaks curl-based canary verification.
|
|
17
|
+
*
|
|
18
|
+
* Fix: read the merged config at request-start and attach it to
|
|
19
|
+
* `event.context.cpubFeatureFlags`. The Vue composable's `getInitialFlags`
|
|
20
|
+
* (modified alongside this plugin) reads from `useRequestEvent()` context
|
|
21
|
+
* on the server, falling back to the runtime config when context is
|
|
22
|
+
* absent (e.g. island components, error pages).
|
|
23
|
+
*
|
|
24
|
+
* Cost: one call to `useConfig()` per request. The merged config is
|
|
25
|
+
* itself cached (60s TTL on DB overrides), so this is a Map lookup,
|
|
26
|
+
* not a DB hit on the hot path.
|
|
27
|
+
*/
|
|
28
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
29
|
+
nitroApp.hooks.hook('request', (event) => {
|
|
30
|
+
try {
|
|
31
|
+
const config = useConfig();
|
|
32
|
+
event.context.cpubFeatureFlags = config.features;
|
|
33
|
+
} catch {
|
|
34
|
+
// useConfig() throws at startup when the DB isn't ready yet.
|
|
35
|
+
// Leave context.cpubFeatureFlags unset → composable falls back to
|
|
36
|
+
// build-time runtime config (same behavior as before this plugin).
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|