@scalemule/news-publication 0.0.38 → 0.0.40

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/README.md CHANGED
@@ -153,3 +153,26 @@ Article integration uses the existing Blog metadata extension:
153
153
  Other modes are `cards` and `features`; optional `area_id` / `feature_ids` select live
154
154
  records. No copied feature snapshot is embedded in a story, so withdrawal takes effect
155
155
  on the next read. Publishers should apply their caption policy to public media copy.
156
+
157
+ ### Votes and moderated conversations
158
+
159
+ Set `config.voting_enabled` and `config.comments_enabled` on the canonical collection
160
+ in the Community Maps editor to enable these controls. Both default to off for
161
+ existing collections. Views share one place record, score, and discussion.
162
+
163
+ The server resolver should include the signed-in reader's `accessToken` from its
164
+ HttpOnly session cookie. Keep the publication API key on the server. Voting
165
+ requires a signed-in reader; guests can read totals and submit comments for review.
166
+ Each reader can upvote (`1`), downvote (`-1`), or clear (`0`) their place vote.
167
+
168
+ The public proxy allows `features/:id/vote` and `comments?feature_id=:id` under
169
+ `collections/:slug`. Without `feature_id`, comments belong to the guide. It strips
170
+ editorial query flags and uses the same signed anonymous identity and origin
171
+ checks as submissions. Comments and replies require the current publication
172
+ permission and always enter review, including comments from signed-in readers.
173
+ Only approved comments whose ancestors remain approved are public.
174
+
175
+ Moderate through **Newsroom → Community Maps → Comments**. Approval requires
176
+ explicit guideline review and the current revision. Existing pumpkin reactions
177
+ remain configurable alongside voting. The UI links to `/account?next=...`; the
178
+ publication must validate that return path and preserve it through sign-in.
@@ -69,11 +69,18 @@ function createPublicationCommunityMapsHandler(options) {
69
69
  const slug = path[1];
70
70
  const rest = path.slice(2);
71
71
  const method = request.method;
72
- const read = method === "GET" && (rest.length === 0 || rest[0] === "features" && (rest.length === 1 || rest.length === 2 && UUID.test(rest[1])) || rest[0] === "media" && rest.length === 2 && UUID.test(rest[1]));
72
+ const discussion = rest.length === 1 && rest[0] === "comments";
73
+ const voting = rest.length === 3 && rest[0] === "features" && UUID.test(rest[1]) && rest[2] === "vote";
74
+ const params = new URL(request.url).searchParams;
75
+ const featureId = params.get("feature_id") || void 0;
76
+ if (discussion && featureId && !UUID.test(featureId))
77
+ return json({ error: { message: "Invalid place" } }, 400);
78
+ const read = method === "GET" && (discussion || voting || rest.length === 0 || rest[0] === "features" && (rest.length === 1 || rest.length === 2 && UUID.test(rest[1])) || rest[0] === "media" && rest.length === 2 && UUID.test(rest[1]));
73
79
  const upload = method === "POST" && rest.length === 3 && rest[0] === "submissions" && UUID.test(rest[1]) && rest[2] === "media";
74
80
  const submission = method === "POST" && rest.length === 1 && rest[0] === "submissions";
75
81
  const action = method === "POST" && rest.length === 3 && rest[0] === "features" && UUID.test(rest[1]) && ["reactions", "reports"].includes(rest[2]);
76
- if (!read && !upload && !submission && !action)
82
+ const engagementWrite = method === "POST" && (discussion || voting);
83
+ if (!read && !upload && !submission && !action && !engagementWrite)
77
84
  return json({ error: { message: "Route not found" } }, 404);
78
85
  const scope = await options.resolve(request);
79
86
  if (method !== "GET" && request.headers.get("origin") !== new URL(scope.origin).origin)
@@ -150,7 +157,25 @@ function createPublicationCommunityMapsHandler(options) {
150
157
  } else {
151
158
  let result;
152
159
  if (read) {
153
- if (!rest.length)
160
+ if (discussion)
161
+ result = await sdk$1.communityMaps.listComments(
162
+ slug,
163
+ {
164
+ feature_id: featureId,
165
+ page: Math.min(
166
+ 1e4,
167
+ Math.max(1, Number(params.get("page")) || 1)
168
+ )
169
+ },
170
+ callOptions
171
+ );
172
+ else if (voting)
173
+ result = await sdk$1.communityMaps.getVote(
174
+ slug,
175
+ rest[1],
176
+ callOptions
177
+ );
178
+ else if (!rest.length)
154
179
  result = await sdk$1.communityMaps.getCollection(slug, callOptions);
155
180
  else if (rest.length === 2)
156
181
  result = await sdk$1.communityMaps.getFeature(
@@ -160,7 +185,7 @@ function createPublicationCommunityMapsHandler(options) {
160
185
  );
161
186
  else {
162
187
  const query = {};
163
- const params = new URL(request.url).searchParams;
188
+ const params2 = new URL(request.url).searchParams;
164
189
  for (const key of [
165
190
  "after",
166
191
  "area_id",
@@ -169,15 +194,15 @@ function createPublicationCommunityMapsHandler(options) {
169
194
  "active_at",
170
195
  "bbox"
171
196
  ]) {
172
- const value = params.get(key);
197
+ const value = params2.get(key);
173
198
  if (value) query[key] = value;
174
199
  }
175
- const limit = params.get("limit");
200
+ const limit = params2.get("limit");
176
201
  if (limit)
177
202
  query.limit = Math.min(200, Math.max(1, Number(limit) || 50));
178
203
  const filters = { ...query };
179
204
  for (const key of ["featured", "verified"]) {
180
- const value = params.get(key);
205
+ const value = params2.get(key);
181
206
  if (value === "true" || value === "false")
182
207
  filters[key] = value === "true";
183
208
  }
@@ -224,7 +249,21 @@ function createPublicationCommunityMapsHandler(options) {
224
249
  }
225
250
  if (!body || typeof body !== "object" || Array.isArray(body))
226
251
  throw new ProxyProblem(400, "Invalid submission.");
227
- if (submission)
252
+ if (discussion)
253
+ result = await sdk$1.communityMaps.createComment(
254
+ slug,
255
+ body,
256
+ featureId,
257
+ callOptions
258
+ );
259
+ else if (voting)
260
+ result = await sdk$1.communityMaps.vote(
261
+ slug,
262
+ rest[1],
263
+ body.value,
264
+ callOptions
265
+ );
266
+ else if (submission)
228
267
  result = await sdk$1.communityMaps.createSubmission(
229
268
  slug,
230
269
  body,
@@ -67,11 +67,18 @@ function createPublicationCommunityMapsHandler(options) {
67
67
  const slug = path[1];
68
68
  const rest = path.slice(2);
69
69
  const method = request.method;
70
- const read = method === "GET" && (rest.length === 0 || rest[0] === "features" && (rest.length === 1 || rest.length === 2 && UUID.test(rest[1])) || rest[0] === "media" && rest.length === 2 && UUID.test(rest[1]));
70
+ const discussion = rest.length === 1 && rest[0] === "comments";
71
+ const voting = rest.length === 3 && rest[0] === "features" && UUID.test(rest[1]) && rest[2] === "vote";
72
+ const params = new URL(request.url).searchParams;
73
+ const featureId = params.get("feature_id") || void 0;
74
+ if (discussion && featureId && !UUID.test(featureId))
75
+ return json({ error: { message: "Invalid place" } }, 400);
76
+ const read = method === "GET" && (discussion || voting || rest.length === 0 || rest[0] === "features" && (rest.length === 1 || rest.length === 2 && UUID.test(rest[1])) || rest[0] === "media" && rest.length === 2 && UUID.test(rest[1]));
71
77
  const upload = method === "POST" && rest.length === 3 && rest[0] === "submissions" && UUID.test(rest[1]) && rest[2] === "media";
72
78
  const submission = method === "POST" && rest.length === 1 && rest[0] === "submissions";
73
79
  const action = method === "POST" && rest.length === 3 && rest[0] === "features" && UUID.test(rest[1]) && ["reactions", "reports"].includes(rest[2]);
74
- if (!read && !upload && !submission && !action)
80
+ const engagementWrite = method === "POST" && (discussion || voting);
81
+ if (!read && !upload && !submission && !action && !engagementWrite)
75
82
  return json({ error: { message: "Route not found" } }, 404);
76
83
  const scope = await options.resolve(request);
77
84
  if (method !== "GET" && request.headers.get("origin") !== new URL(scope.origin).origin)
@@ -148,7 +155,25 @@ function createPublicationCommunityMapsHandler(options) {
148
155
  } else {
149
156
  let result;
150
157
  if (read) {
151
- if (!rest.length)
158
+ if (discussion)
159
+ result = await sdk.communityMaps.listComments(
160
+ slug,
161
+ {
162
+ feature_id: featureId,
163
+ page: Math.min(
164
+ 1e4,
165
+ Math.max(1, Number(params.get("page")) || 1)
166
+ )
167
+ },
168
+ callOptions
169
+ );
170
+ else if (voting)
171
+ result = await sdk.communityMaps.getVote(
172
+ slug,
173
+ rest[1],
174
+ callOptions
175
+ );
176
+ else if (!rest.length)
152
177
  result = await sdk.communityMaps.getCollection(slug, callOptions);
153
178
  else if (rest.length === 2)
154
179
  result = await sdk.communityMaps.getFeature(
@@ -158,7 +183,7 @@ function createPublicationCommunityMapsHandler(options) {
158
183
  );
159
184
  else {
160
185
  const query = {};
161
- const params = new URL(request.url).searchParams;
186
+ const params2 = new URL(request.url).searchParams;
162
187
  for (const key of [
163
188
  "after",
164
189
  "area_id",
@@ -167,15 +192,15 @@ function createPublicationCommunityMapsHandler(options) {
167
192
  "active_at",
168
193
  "bbox"
169
194
  ]) {
170
- const value = params.get(key);
195
+ const value = params2.get(key);
171
196
  if (value) query[key] = value;
172
197
  }
173
- const limit = params.get("limit");
198
+ const limit = params2.get("limit");
174
199
  if (limit)
175
200
  query.limit = Math.min(200, Math.max(1, Number(limit) || 50));
176
201
  const filters = { ...query };
177
202
  for (const key of ["featured", "verified"]) {
178
- const value = params.get(key);
203
+ const value = params2.get(key);
179
204
  if (value === "true" || value === "false")
180
205
  filters[key] = value === "true";
181
206
  }
@@ -222,7 +247,21 @@ function createPublicationCommunityMapsHandler(options) {
222
247
  }
223
248
  if (!body || typeof body !== "object" || Array.isArray(body))
224
249
  throw new ProxyProblem(400, "Invalid submission.");
225
- if (submission)
250
+ if (discussion)
251
+ result = await sdk.communityMaps.createComment(
252
+ slug,
253
+ body,
254
+ featureId,
255
+ callOptions
256
+ );
257
+ else if (voting)
258
+ result = await sdk.communityMaps.vote(
259
+ slug,
260
+ rest[1],
261
+ body.value,
262
+ callOptions
263
+ );
264
+ else if (submission)
226
265
  result = await sdk.communityMaps.createSubmission(
227
266
  slug,
228
267
  body,
@@ -40,6 +40,7 @@ function PublicationCommunityMapPage({
40
40
  () => communityMaps.createCommunityMapsBrowserClient(apiPath),
41
41
  [apiPath]
42
42
  );
43
+ const signInHref = (f) => `/account?next=${encodeURIComponent(`${basePath}/places/${f.id}`)}`;
43
44
  const featureHref = (f) => `${basePath}/places/${f.id}`;
44
45
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "smcm", children: [
45
46
  mode !== "guide" && /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsxs("a", { href: basePath, children: [
@@ -62,6 +63,7 @@ function PublicationCommunityMapPage({
62
63
  client,
63
64
  collection,
64
65
  feature,
66
+ signInHref: signInHref(feature),
65
67
  correctionHref: `${basePath}/submit?correction=${feature.id}&area=${feature.area_id}`
66
68
  }
67
69
  ) : atlas ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -71,6 +73,7 @@ function PublicationCommunityMapPage({
71
73
  collection,
72
74
  atlas,
73
75
  featureHref,
76
+ signInHref,
74
77
  communityHref: (id) => `${basePath}?area=${id}`,
75
78
  submitHref: `${basePath}/submit?area=${collection.areas.find((a) => a.slug === atlas.areaSlug)?.id || ""}`
76
79
  }
@@ -80,11 +83,21 @@ function PublicationCommunityMapPage({
80
83
  client,
81
84
  collection,
82
85
  featureHref,
86
+ signInHref,
83
87
  communityHref: (a) => `${basePath}/map?area=${a.id}`,
84
88
  submitHref: `${basePath}/submit${areaId ? `?area=${areaId}` : ""}`,
85
89
  initialAreaId: areaId,
86
90
  showMap: mode === "map"
87
91
  }
92
+ ),
93
+ (mode === "guide" || mode === "map") && /* @__PURE__ */ jsxRuntime.jsx(
94
+ communityMaps.CommunityDiscussion,
95
+ {
96
+ client,
97
+ collection,
98
+ guidelinesHref: "/editorial-standards",
99
+ termsHref: "/terms"
100
+ }
88
101
  )
89
102
  ] });
90
103
  }
@@ -154,6 +167,9 @@ function ArticleMap({
154
167
  communityMaps.CommunityFeatureCard,
155
168
  {
156
169
  feature: f,
170
+ client,
171
+ collection: data.collection,
172
+ signInHref: `/account?next=${encodeURIComponent(`${path}/places/${f.id}`)}`,
157
173
  href: `${path}/places/${f.id}`,
158
174
  photoUrl: f.media[0] ? client.mediaUrl(data.collection.slug, f.media[0].id) : void 0
159
175
  },
@@ -2,7 +2,7 @@
2
2
  import { communityMapEmbeds } from './chunk-IMYBSG7J.js';
3
3
  export { communityMapEmbeds } from './chunk-IMYBSG7J.js';
4
4
  import { useMemo, useState, useEffect } from 'react';
5
- import { createCommunityMapsBrowserClient, CommunitySubmissionForm, CommunityFeatureDetail, CommunityCityGuide, CommunityGuide, CommunityCards, CommunityMap, CommunityFeatureCard } from '@scalemule/react/community-maps';
5
+ import { createCommunityMapsBrowserClient, CommunitySubmissionForm, CommunityFeatureDetail, CommunityCityGuide, CommunityGuide, CommunityDiscussion, CommunityCards, CommunityMap, CommunityFeatureCard } from '@scalemule/react/community-maps';
6
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
7
 
8
8
  function PublicationCommunityMapPage({
@@ -19,6 +19,7 @@ function PublicationCommunityMapPage({
19
19
  () => createCommunityMapsBrowserClient(apiPath),
20
20
  [apiPath]
21
21
  );
22
+ const signInHref = (f) => `/account?next=${encodeURIComponent(`${basePath}/places/${f.id}`)}`;
22
23
  const featureHref = (f) => `${basePath}/places/${f.id}`;
23
24
  return /* @__PURE__ */ jsxs("div", { className: "smcm", children: [
24
25
  mode !== "guide" && /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsxs("a", { href: basePath, children: [
@@ -41,6 +42,7 @@ function PublicationCommunityMapPage({
41
42
  client,
42
43
  collection,
43
44
  feature,
45
+ signInHref: signInHref(feature),
44
46
  correctionHref: `${basePath}/submit?correction=${feature.id}&area=${feature.area_id}`
45
47
  }
46
48
  ) : atlas ? /* @__PURE__ */ jsx(
@@ -50,6 +52,7 @@ function PublicationCommunityMapPage({
50
52
  collection,
51
53
  atlas,
52
54
  featureHref,
55
+ signInHref,
53
56
  communityHref: (id) => `${basePath}?area=${id}`,
54
57
  submitHref: `${basePath}/submit?area=${collection.areas.find((a) => a.slug === atlas.areaSlug)?.id || ""}`
55
58
  }
@@ -59,11 +62,21 @@ function PublicationCommunityMapPage({
59
62
  client,
60
63
  collection,
61
64
  featureHref,
65
+ signInHref,
62
66
  communityHref: (a) => `${basePath}/map?area=${a.id}`,
63
67
  submitHref: `${basePath}/submit${areaId ? `?area=${areaId}` : ""}`,
64
68
  initialAreaId: areaId,
65
69
  showMap: mode === "map"
66
70
  }
71
+ ),
72
+ (mode === "guide" || mode === "map") && /* @__PURE__ */ jsx(
73
+ CommunityDiscussion,
74
+ {
75
+ client,
76
+ collection,
77
+ guidelinesHref: "/editorial-standards",
78
+ termsHref: "/terms"
79
+ }
67
80
  )
68
81
  ] });
69
82
  }
@@ -133,6 +146,9 @@ function ArticleMap({
133
146
  CommunityFeatureCard,
134
147
  {
135
148
  feature: f,
149
+ client,
150
+ collection: data.collection,
151
+ signInHref: `/account?next=${encodeURIComponent(`${path}/places/${f.id}`)}`,
136
152
  href: `${path}/places/${f.id}`,
137
153
  photoUrl: f.media[0] ? client.mediaUrl(data.collection.slug, f.media[0].id) : void 0
138
154
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "description": "Shared server framework for ScaleMule local news publications. Walnut Creek Times is the broadsheet default; each paper supplies its own identity and feature flags.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -110,7 +110,7 @@
110
110
  ],
111
111
  "peerDependencies": {
112
112
  "react": ">=18.0.0",
113
- "@scalemule/react": ">=0.0.13 <0.1.0"
113
+ "@scalemule/react": "0.0.16"
114
114
  },
115
115
  "peerDependenciesMeta": {
116
116
  "react": {
@@ -139,7 +139,7 @@
139
139
  "tsup": "^8.5.0",
140
140
  "typescript": "^5.8.3",
141
141
  "vitest": "^3.2.4",
142
- "@scalemule/react": "^0.0.13"
142
+ "@scalemule/react": "0.0.16"
143
143
  },
144
144
  "keywords": [
145
145
  "scalemule",
@@ -162,7 +162,7 @@
162
162
  },
163
163
  "dependencies": {
164
164
  "@scalemule/compliance": "^0.0.5",
165
- "@scalemule/sdk": "0.0.73",
166
- "@scalemule/react": "0.0.14"
165
+ "@scalemule/sdk": "0.0.74",
166
+ "@scalemule/react": "0.0.16"
167
167
  }
168
168
  }