@commonpub/layer 0.10.0 → 0.11.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/homepage/ContentGridSection.vue +133 -0
- package/components/homepage/ContestsSection.vue +39 -0
- package/components/homepage/CustomHtmlSection.vue +20 -0
- package/components/homepage/EditorialSection.vue +32 -0
- package/components/homepage/HeroSection.vue +73 -0
- package/components/homepage/HomepageSectionRenderer.vue +64 -0
- package/components/homepage/HubsSection.vue +66 -0
- package/components/homepage/StatsSection.vue +38 -0
- package/layouts/admin.vue +2 -0
- package/package.json +5 -5
- package/pages/admin/features.vue +338 -0
- package/pages/admin/homepage.vue +292 -0
- package/pages/index.vue +34 -1
- package/server/api/admin/categories/[id].delete.ts +5 -2
- package/server/api/admin/features/index.get.ts +32 -0
- package/server/api/admin/features/index.put.ts +56 -0
- package/server/api/admin/homepage/sections.get.ts +11 -0
- package/server/api/admin/homepage/sections.put.ts +52 -0
- package/server/api/features.get.ts +9 -0
- package/server/api/homepage/sections.get.ts +10 -0
|
@@ -9,8 +9,11 @@ export default defineEventHandler(async (event) => {
|
|
|
9
9
|
const db = useDB();
|
|
10
10
|
const { id } = parseParams(event, { id: 'uuid' });
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
if (!deleted) {
|
|
12
|
+
const result = await deleteContentCategory(db, id);
|
|
13
|
+
if (!result.deleted) {
|
|
14
|
+
if (result.error === 'system_category') {
|
|
15
|
+
throw createError({ statusCode: 403, statusMessage: 'System categories cannot be deleted' });
|
|
16
|
+
}
|
|
14
17
|
throw createError({ statusCode: 404, statusMessage: 'Category not found' });
|
|
15
18
|
}
|
|
16
19
|
return { success: true };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getInstanceSetting } from '@commonpub/server';
|
|
2
|
+
import type { FeatureFlags } from '@commonpub/config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/admin/features
|
|
6
|
+
* Returns current feature flags with metadata about defaults vs overrides.
|
|
7
|
+
*/
|
|
8
|
+
export default defineEventHandler(async (event) => {
|
|
9
|
+
requireAdmin(event);
|
|
10
|
+
|
|
11
|
+
const db = useDB();
|
|
12
|
+
const config = useConfig();
|
|
13
|
+
|
|
14
|
+
// Get DB overrides (may be null if never set)
|
|
15
|
+
const raw = await getInstanceSetting(db, 'features.overrides');
|
|
16
|
+
const overrides: Partial<FeatureFlags> = (raw && typeof raw === 'object' && !Array.isArray(raw))
|
|
17
|
+
? raw as Partial<FeatureFlags>
|
|
18
|
+
: {};
|
|
19
|
+
|
|
20
|
+
// Build response with default + effective values for each flag
|
|
21
|
+
const flags = config.features as unknown as Record<string, boolean>;
|
|
22
|
+
const result: Record<string, { enabled: boolean; isOverridden: boolean }> = {};
|
|
23
|
+
|
|
24
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
25
|
+
result[key] = {
|
|
26
|
+
enabled: value,
|
|
27
|
+
isOverridden: key in overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { flags: result, overrides };
|
|
32
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { setInstanceSetting, getInstanceSetting } from '@commonpub/server';
|
|
2
|
+
import type { FeatureFlags } from '@commonpub/config';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const updateFeaturesSchema = z.object({
|
|
6
|
+
overrides: z.record(z.string(), z.boolean()).refine(
|
|
7
|
+
(obj) => Object.keys(obj).length <= 20,
|
|
8
|
+
'Too many overrides',
|
|
9
|
+
),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* PUT /api/admin/features
|
|
14
|
+
* Set feature flag overrides. Pass { overrides: { flagName: true/false } }.
|
|
15
|
+
* To remove an override, omit the key from overrides.
|
|
16
|
+
*/
|
|
17
|
+
export default defineEventHandler(async (event) => {
|
|
18
|
+
const user = requireAdmin(event);
|
|
19
|
+
|
|
20
|
+
const body = await parseBody(event, updateFeaturesSchema);
|
|
21
|
+
const db = useDB();
|
|
22
|
+
|
|
23
|
+
// Validate that all keys are known feature flags
|
|
24
|
+
const config = useConfig();
|
|
25
|
+
const knownFlags = Object.keys(config.features);
|
|
26
|
+
for (const key of Object.keys(body.overrides)) {
|
|
27
|
+
if (!knownFlags.includes(key)) {
|
|
28
|
+
throw createError({ statusCode: 400, statusMessage: `Unknown feature flag: ${key}` });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Merge with existing overrides (so partial updates work)
|
|
33
|
+
const raw = await getInstanceSetting(db, 'features.overrides');
|
|
34
|
+
const existing: Partial<FeatureFlags> = (raw && typeof raw === 'object' && !Array.isArray(raw))
|
|
35
|
+
? raw as Partial<FeatureFlags>
|
|
36
|
+
: {};
|
|
37
|
+
|
|
38
|
+
const merged = { ...existing, ...body.overrides };
|
|
39
|
+
|
|
40
|
+
// Remove overrides that match the base config default (no point overriding to same value)
|
|
41
|
+
const base = config.features as unknown as Record<string, boolean>;
|
|
42
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
43
|
+
if (base[key] === value) {
|
|
44
|
+
delete (merged as Record<string, unknown>)[key];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await setInstanceSetting(db, 'features.overrides', merged, user.id, getRequestIP(event) ?? undefined);
|
|
49
|
+
|
|
50
|
+
// Invalidate config cache so the change takes effect immediately
|
|
51
|
+
if (typeof invalidateConfigCache === 'function') {
|
|
52
|
+
invalidateConfigCache();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { overrides: merged, message: 'Feature flags updated' };
|
|
56
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getHomepageSections } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GET /api/admin/homepage/sections
|
|
5
|
+
* Returns homepage sections for admin editing.
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async (event) => {
|
|
8
|
+
requireAdmin(event);
|
|
9
|
+
const db = useDB();
|
|
10
|
+
return getHomepageSections(db);
|
|
11
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { setHomepageSections } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const sectionConfigSchema = z.object({
|
|
5
|
+
contentType: z.string().max(64).optional(),
|
|
6
|
+
sort: z.enum(['popular', 'recent', 'featured', 'editorial']).optional(),
|
|
7
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
8
|
+
columns: z.union([z.literal(2), z.literal(3), z.literal(4)]).optional(),
|
|
9
|
+
showEditorial: z.boolean().optional(),
|
|
10
|
+
categorySlug: z.string().max(64).optional(),
|
|
11
|
+
featureGate: z.string().max(64).optional(),
|
|
12
|
+
variant: z.string().max(64).optional(),
|
|
13
|
+
customTitle: z.string().max(255).optional(),
|
|
14
|
+
customSubtitle: z.string().max(500).optional(),
|
|
15
|
+
html: z.string().max(10000).optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const sectionSchema = z.object({
|
|
19
|
+
id: z.string().min(1).max(64),
|
|
20
|
+
type: z.enum(['hero', 'editorial', 'content-grid', 'content-carousel', 'contests', 'hubs', 'learning', 'stats', 'custom-html']),
|
|
21
|
+
title: z.string().max(255).optional(),
|
|
22
|
+
enabled: z.boolean(),
|
|
23
|
+
order: z.number().int().min(0),
|
|
24
|
+
config: sectionConfigSchema,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const updateSectionsSchema = z.object({
|
|
28
|
+
sections: z.array(sectionSchema).min(1).max(20),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* PUT /api/admin/homepage/sections
|
|
33
|
+
* Save homepage section configuration.
|
|
34
|
+
*/
|
|
35
|
+
export default defineEventHandler(async (event) => {
|
|
36
|
+
const user = requireAdmin(event);
|
|
37
|
+
const db = useDB();
|
|
38
|
+
const body = await parseBody(event, updateSectionsSchema);
|
|
39
|
+
|
|
40
|
+
// Validate unique IDs
|
|
41
|
+
const ids = new Set<string>();
|
|
42
|
+
for (const section of body.sections) {
|
|
43
|
+
if (ids.has(section.id)) {
|
|
44
|
+
throw createError({ statusCode: 400, statusMessage: `Duplicate section ID: ${section.id}` });
|
|
45
|
+
}
|
|
46
|
+
ids.add(section.id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await setHomepageSections(db, body.sections, user.id, getRequestIP(event) ?? undefined);
|
|
50
|
+
|
|
51
|
+
return { sections: body.sections, message: 'Homepage updated' };
|
|
52
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/features
|
|
3
|
+
* Returns the current merged feature flags (build-time + env + DB overrides).
|
|
4
|
+
* Public endpoint — used by client-side useFeatures() for runtime reactivity.
|
|
5
|
+
*/
|
|
6
|
+
export default defineEventHandler(() => {
|
|
7
|
+
const config = useConfig();
|
|
8
|
+
return config.features;
|
|
9
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getHomepageSections } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GET /api/homepage/sections
|
|
5
|
+
* Returns the homepage section configuration (public).
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler(async () => {
|
|
8
|
+
const db = useDB();
|
|
9
|
+
return getHomepageSections(db);
|
|
10
|
+
});
|