@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 +23 -0
- package/dist/community-maps-server.cjs +47 -8
- package/dist/community-maps-server.js +47 -8
- package/dist/community-maps-view.cjs +16 -0
- package/dist/community-maps-view.js +17 -1
- package/package.json +5 -5
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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 =
|
|
197
|
+
const value = params2.get(key);
|
|
173
198
|
if (value) query[key] = value;
|
|
174
199
|
}
|
|
175
|
-
const 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 =
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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 =
|
|
195
|
+
const value = params2.get(key);
|
|
171
196
|
if (value) query[key] = value;
|
|
172
197
|
}
|
|
173
|
-
const 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 =
|
|
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 (
|
|
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.
|
|
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": "
|
|
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": "
|
|
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.
|
|
166
|
-
"@scalemule/react": "0.0.
|
|
165
|
+
"@scalemule/sdk": "0.0.74",
|
|
166
|
+
"@scalemule/react": "0.0.16"
|
|
167
167
|
}
|
|
168
168
|
}
|