Skip to content

Commit 857b183

Browse files
authored
Merge pull request #63 from raid-guild/feat/widget-summaries
Feat/widget summaries
2 parents dc31b06 + c778409 commit 857b183

12 files changed

Lines changed: 959 additions & 58 deletions

File tree

.github/ISSUE_TEMPLATE/module-spec.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,32 @@ body:
5959
validations:
6060
required: true
6161

62+
- type: dropdown
63+
id: summary_layout
64+
attributes:
65+
label: Summary layout
66+
description: "How should the module summary render in cards?"
67+
options:
68+
- list
69+
- excerpt
70+
- compact
71+
- widget
72+
- none
73+
validations:
74+
required: true
75+
76+
- type: dropdown
77+
id: summary_widget_mode
78+
attributes:
79+
label: Summary widget mode (if widget)
80+
description: "If Summary layout is widget, choose data (portal-rendered) or embed (iframe)."
81+
options:
82+
- not-applicable
83+
- data
84+
- embed
85+
validations:
86+
required: true
87+
6288
- type: dropdown
6389
id: storage
6490
attributes:

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ Behavior notes:
111111
- `compact`: small stats/data tile
112112
- `default`: standard stackable module card
113113
- `wide`: full-width module card
114+
- Summary cards can also render dynamic widgets via `summary.layout: "widget"` and
115+
`summary.widget` (data-rendered or embedded iframe).
114116

115117
## Supabase
116118
- Migrations and seeds live in `supabase/`

docs/module-authoring.md

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,80 @@ The summary endpoint should return:
198198

199199
### Summary presentation options
200200
Optional fields for summary layout:
201-
- `layout`: `list` (default), `excerpt`, or `compact`
201+
- `layout`: `list` (default), `excerpt`, `compact`, or `widget`
202202
- `maxItems`: limit the number of items shown
203203
- `truncate`: max character length for values or excerpts
204204
- `showTitle`: boolean to show/hide the summary title
205205
- `progressKey`: render a progress bar for the matching item label
206206
- `imageKey`: render a circular thumbnail for the matching item label (compact layout)
207+
- `widget`: configuration for dynamic summary widgets when `layout` is `widget`
208+
209+
### Summary widget primitive
210+
Use `layout: "widget"` when a summary card should render dynamic content.
211+
212+
Supported widget modes:
213+
- `mode: "data"`: endpoint returns generic `widget.data`; portal renders a supported widget component.
214+
- `mode: "embed"`: portal renders an iframe from `widget.src` (use for third-party mini apps).
215+
216+
Widget config shape:
217+
```json
218+
{
219+
"summary": {
220+
"source": "api",
221+
"endpoint": "/api/modules/example/summary",
222+
"layout": "widget",
223+
"widget": {
224+
"mode": "data",
225+
"type": "chart",
226+
"variant": "force-graph",
227+
"height": 220
228+
}
229+
}
230+
}
231+
```
232+
233+
Endpoint response shape (with fallback):
234+
```json
235+
{
236+
"title": "Example Summary",
237+
"items": [
238+
{ "label": "People", "value": "42" }
239+
],
240+
"widget": {
241+
"mode": "data",
242+
"type": "chart",
243+
"variant": "force-graph",
244+
"data": {
245+
"nodes": [{ "id": "skill:Solidity", "label": "Solidity", "group": "skill" }],
246+
"links": []
247+
}
248+
}
249+
}
250+
```
251+
252+
Notes:
253+
- Always include `items` as a fallback for clients/surfaces that cannot render the widget.
254+
- For third-party widgets, prefer `mode: "embed"` with explicit origins and auth constraints.
255+
256+
Example: third-party embedded widget summary
257+
```json
258+
{
259+
"allowedOrigins": ["https://widgets.example.com"],
260+
"summaryBySurface": {
261+
"home": {
262+
"source": "api",
263+
"title": "Module Snapshot",
264+
"endpoint": "/api/modules/example/summary",
265+
"layout": "widget",
266+
"widget": {
267+
"mode": "embed",
268+
"src": "https://widgets.example.com/summary?module=example",
269+
"height": 220
270+
}
271+
}
272+
}
273+
}
274+
```
207275

208276
Example with excerpt:
209277
```json

modules/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,14 @@ Control module card size in surface grids via `presentation.layout`:
2323

2424
If omitted, the portal currently treats layout as `default`.
2525

26+
## Dynamic Summary Widgets
27+
Summary cards can render richer dynamic content by using:
28+
- `summary.layout: "widget"`
29+
- `summary.widget.mode: "data"` (portal-rendered widget data), or
30+
- `summary.widget.mode: "embed"` (iframe widget URL).
31+
32+
Include summary `items` as fallback content for clients/surfaces that cannot render the
33+
widget.
34+
2635
## Host-only Modules
2736
Add the `hosts` tag to hide a module from non-hosts. Roles are managed in `public.user_roles`.

modules/registry.json

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,8 @@
404404
},
405405
"requiresAuth": true,
406406
"tags": [
407-
"me-tools"
407+
"me-tools",
408+
"member-tools"
408409
],
409410
"url": "/modules/dao-membership",
410411
"presentation": {
@@ -468,7 +469,6 @@
468469
},
469470
"requiresAuth": true,
470471
"tags": [
471-
"start-here",
472472
"hosts",
473473
"host-tools"
474474
],
@@ -559,17 +559,20 @@
559559
"presentation": {
560560
"mode": "page",
561561
"trigger": "button",
562-
"layout": "default"
563-
},
564-
"access": {
565-
"requiresAuth": true,
566-
"entitlement": "cohort-access"
562+
"layout": "wide"
567563
},
568564
"summaryBySurface": {
569565
"home": {
570566
"source": "api",
571567
"title": "Cohort Skills",
572-
"endpoint": "/api/modules/skills-explorer/summary"
568+
"endpoint": "/api/modules/skills-explorer/summary/widget",
569+
"layout": "widget",
570+
"widget": {
571+
"mode": "data",
572+
"type": "chart",
573+
"variant": "force-graph",
574+
"height": 220
575+
}
573576
},
574577
"people": {
575578
"source": "api",
@@ -738,7 +741,7 @@
738741
},
739742
"requiresAuth": true,
740743
"tags": [
741-
"start-here",
744+
"people-tools",
742745
"me-tools",
743746
"profile-tools-public"
744747
],
@@ -771,6 +774,7 @@
771774
},
772775
"requiresAuth": true,
773776
"tags": [
777+
"start-here",
774778
"people-tools"
775779
],
776780
"presentation": {
@@ -801,6 +805,7 @@
801805
},
802806
"requiresAuth": false,
803807
"tags": [
808+
"start-here",
804809
"people-tools"
805810
],
806811
"presentation": {
@@ -896,6 +901,59 @@
896901
"showTitle": true
897902
}
898903
},
904+
{
905+
"id": "dao-handbook",
906+
"title": "DAO Handbook",
907+
"description": "Open DAO docs, policies, and contributor guidance in one place.",
908+
"lane": "cohort",
909+
"type": "link",
910+
"url": "https://rg-handbook-nextra.vercel.app/",
911+
"status": "live",
912+
"owner": {
913+
"name": "@dekanbro",
914+
"contact": "@dekanbro"
915+
},
916+
"requiresAuth": true,
917+
"access": {
918+
"requiresAuth": true,
919+
"entitlement": "dao-member"
920+
},
921+
"tags": [
922+
"member-tools"
923+
],
924+
"presentation": {
925+
"mode": "dialog",
926+
"trigger": "button",
927+
"dialog": {
928+
"size": "lg"
929+
},
930+
"iframe": {
931+
"height": 820
932+
},
933+
"layout": "default"
934+
},
935+
"summary": {
936+
"source": "static",
937+
"title": "DAO Handbook",
938+
"items": [
939+
{
940+
"label": "Governance docs",
941+
"value": "Browse rules, voting norms, and governance references."
942+
},
943+
{
944+
"label": "Operations guides",
945+
"value": "Find practical workflows for running DAO work."
946+
},
947+
{
948+
"label": "Contributor context",
949+
"value": "Review expectations and orientation material."
950+
}
951+
],
952+
"layout": "list",
953+
"maxItems": 3,
954+
"showTitle": true
955+
}
956+
},
899957
{
900958
"id": "module-requests",
901959
"title": "Module Requests",

src/app/api/modules/skills-explorer/summary/route.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,51 @@
1+
import { supabaseAdminClient } from "@/lib/supabase/admin";
12
import { loadPeople } from "@/lib/people";
3+
import { supabaseServerClient } from "@/lib/supabase/server";
24

3-
export async function GET() {
5+
type ViewTier = "public" | "authenticated" | "dao-member";
6+
7+
async function resolveTier(request: Request): Promise<ViewTier> {
8+
const authHeader = request.headers.get("authorization");
9+
if (!authHeader?.startsWith("Bearer ")) {
10+
return "public";
11+
}
12+
13+
const token = authHeader.replace("Bearer ", "");
14+
let userId: string;
15+
try {
16+
const supabase = supabaseServerClient();
17+
const { data: userData, error: userError } = await supabase.auth.getUser(token);
18+
if (userError || !userData.user) {
19+
return "public";
20+
}
21+
userId = userData.user.id;
22+
} catch {
23+
return "public";
24+
}
25+
26+
try {
27+
const admin = supabaseAdminClient();
28+
const now = new Date().toISOString();
29+
const { data, error } = await admin
30+
.from("entitlements")
31+
.select("entitlement")
32+
.eq("user_id", userId)
33+
.eq("status", "active")
34+
.or(`expires_at.is.null,expires_at.gt.${now}`);
35+
36+
if (error || !data) {
37+
return "authenticated";
38+
}
39+
40+
const entitlements = new Set(data.map((row) => row.entitlement));
41+
return entitlements.has("dao-member") ? "dao-member" : "authenticated";
42+
} catch {
43+
return "authenticated";
44+
}
45+
}
46+
47+
export async function GET(request: Request) {
48+
const tier = await resolveTier(request);
449
const people = await loadPeople();
550
const allSkills = people.flatMap((person) => person.skills ?? []);
651
const uniqueSkills = new Set(allSkills);
@@ -14,12 +59,20 @@ export async function GET() {
1459
.sort((a, b) => b[1] - a[1])
1560
.at(0);
1661

62+
const visibility =
63+
tier === "dao-member"
64+
? "DAO detail"
65+
: tier === "authenticated"
66+
? "Authenticated aggregate"
67+
: "Public aggregate";
68+
1769
return Response.json({
1870
title: "Cohort Skills",
1971
items: [
2072
{ label: "People", value: String(people.length) },
2173
{ label: "Unique skills", value: String(uniqueSkills.size) },
2274
{ label: "Top skill", value: top ? `${top[0]}` : "TBD" },
75+
{ label: "View", value: visibility },
2376
],
2477
});
2578
}

0 commit comments

Comments
 (0)