@scalemule/news-publication 0.0.28 → 0.0.29

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.
@@ -77,6 +77,13 @@ function StoryReview({
77
77
  const thanksRef = react.useRef(null);
78
78
  const [moreQuestions, setMoreQuestions] = react.useState(false);
79
79
  const [correctionKind, setCorrectionKind] = react.useState("Correct a detail");
80
+ const [replacementPhoto, setReplacementPhoto] = react.useState(null);
81
+ function openMedia(photo = null) {
82
+ setReplacementPhoto(photo);
83
+ setFile(null);
84
+ setRights(false);
85
+ setPanel("media");
86
+ }
80
87
  react.useEffect(() => {
81
88
  if (panel !== "article") {
82
89
  responseRef.current?.scrollIntoView({
@@ -197,13 +204,34 @@ function StoryReview({
197
204
  "metadata",
198
205
  JSON.stringify({ ...mediaInfo, rights_confirmed: true })
199
206
  );
200
- await request("/media", form);
207
+ const received = await request("/media", form);
208
+ let uploadNotice = "Media received privately. The newsroom will review it before publication.";
209
+ if (replacementPhoto) {
210
+ uploadNotice = "Your replacement photo has been sent to the newsroom. The current photo will stay in this preview until we prepare an updated draft.";
211
+ if (can("COMMENT")) {
212
+ try {
213
+ await request("", {
214
+ revision_id: review.revision.id,
215
+ action: "COMMENT",
216
+ message: "Please use my newly uploaded photograph in place of this photo.",
217
+ target: {
218
+ kind: "PHOTO",
219
+ reference: replacementPhoto.contribution_id,
220
+ replacement_contribution_id: received.id
221
+ }
222
+ });
223
+ } catch {
224
+ uploadNotice = "Your photo was uploaded privately, but we couldn't attach the replacement note. Please use Suggest a Change to tell the newsroom which photograph it should replace.";
225
+ }
226
+ } else {
227
+ uploadNotice = "Your photo was uploaded privately for newsroom review. The newsroom will select the photo for the next preview.";
228
+ }
229
+ }
201
230
  await load();
202
231
  setFile(null);
203
232
  setRights(false);
204
- setNotice(
205
- "Media received privately. The newsroom will review it before publication."
206
- );
233
+ setNotice(uploadNotice);
234
+ setReplacementPhoto(null);
207
235
  setPanel("article");
208
236
  } catch (e) {
209
237
  setError(e instanceof Error ? e.message : "Upload failed.");
@@ -334,7 +362,16 @@ function StoryReview({
334
362
  /* @__PURE__ */ jsxRuntime.jsx("h1", { children: review.revision.document.title }),
335
363
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "sm-review-dek", children: review.revision.document.subtitle }),
336
364
  /* @__PURE__ */ jsxRuntime.jsx(EditorialNotices, { editorial: review.revision.editorial }),
337
- review.revision.document.media.filter((m) => m.position === "hero").map((m) => /* @__PURE__ */ jsxRuntime.jsx(PrivatePhoto, { media: m, base }, m.file_id)),
365
+ review.revision.document.media.filter((m) => m.position === "hero").map((m) => /* @__PURE__ */ jsxRuntime.jsx(
366
+ PrivatePhoto,
367
+ {
368
+ media: m,
369
+ base,
370
+ onChange: can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
371
+ disabled: busy
372
+ },
373
+ m.file_id
374
+ )),
338
375
  celebration && !review.revision.document.media.some(
339
376
  (m) => m.position === "hero"
340
377
  ) && can("UPLOAD_MEDIA") && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -347,7 +384,7 @@ function StoryReview({
347
384
  /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "A photograph makes it yours." }),
348
385
  /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Family photograph may be added before publication." })
349
386
  ] }),
350
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("media"), children: "+ Add a photograph" })
387
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => openMedia(), children: "+ Add a photograph" })
351
388
  ]
352
389
  }
353
390
  ),
@@ -358,7 +395,16 @@ function StoryReview({
358
395
  dangerouslySetInnerHTML: { __html: review.revision.content_html }
359
396
  }
360
397
  ),
361
- review.revision.document.media.filter((m) => m.position !== "hero").map((m) => /* @__PURE__ */ jsxRuntime.jsx(PrivatePhoto, { media: m, base }, m.file_id))
398
+ review.revision.document.media.filter((m) => m.position !== "hero").map((m) => /* @__PURE__ */ jsxRuntime.jsx(
399
+ PrivatePhoto,
400
+ {
401
+ media: m,
402
+ base,
403
+ onChange: m.kind !== "video" && can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
404
+ disabled: busy
405
+ },
406
+ m.file_id
407
+ ))
362
408
  ] }),
363
409
  celebration && /* @__PURE__ */ jsxRuntime.jsxs("section", { id: "review-options", className: "sm-review-welcome", children: [
364
410
  /* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Help us make this just right" }),
@@ -401,7 +447,7 @@ function StoryReview({
401
447
  ),
402
448
  can("REQUEST_CHANGES") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("changes"), children: celebration ? "Suggest a Change" : "Request changes" }),
403
449
  celebration && can("PROPOSE_EDITS") && !can("REQUEST_CHANGES") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("edits"), children: "Suggest a Change" }),
404
- can("UPLOAD_MEDIA") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("media"), children: celebration ? "Add Photos" : "Add photos" }),
450
+ can("UPLOAD_MEDIA") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => openMedia(), children: celebration ? "Add Photos" : "Add photos" }),
405
451
  !celebration && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
406
452
  can("PROPOSE_EDITS") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("edits"), children: "Suggest edits" }),
407
453
  can("ANSWER_QUESTIONS") && review.questions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("questions"), children: "Answer questions" }),
@@ -423,7 +469,7 @@ function StoryReview({
423
469
  "aria-label": "Your response",
424
470
  children: [
425
471
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-form-heading", children: [
426
- /* @__PURE__ */ jsxRuntime.jsx("h2", { children: panel === "approve" ? "Approve this exact draft" : panel === "edits" ? "Propose changes" : panel === "media" ? "Contribute a photograph" : panel === "questions" ? "Tell us a little more" : panel === "history" ? "Selected revision history" : panel === "decline" ? "Prefer not to publish" : panel === "changes" ? "Suggest a change" : "Message the newsroom" }),
472
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { children: panel === "approve" ? "Approve this exact draft" : panel === "edits" ? "Propose changes" : panel === "media" ? replacementPhoto ? "Change photo" : "Contribute a photograph" : panel === "questions" ? "Tell us a little more" : panel === "history" ? "Selected revision history" : panel === "decline" ? "Prefer not to publish" : panel === "changes" ? "Suggest a change" : "Message the newsroom" }),
427
473
  /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Close" })
428
474
  ] }),
429
475
  panel === "edits" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -562,6 +608,7 @@ function StoryReview({
562
608
  void upload();
563
609
  },
564
610
  children: [
611
+ replacementPhoto && /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Choose the photograph you'd like us to use instead. We'll review it and prepare an updated preview; the current photo stays in place until then." }),
565
612
  /* @__PURE__ */ jsxRuntime.jsx("p", { children: "JPEG, PNG or WebP photos up to 12 MB; MP4 or WebM videos up to 25 MB. Media remain private until selected and approved by the newsroom." }),
566
613
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
567
614
  "Photograph or video",
@@ -847,7 +894,7 @@ function StoryReview({
847
894
  publicationFooter
848
895
  ] });
849
896
  }
850
- function PrivatePhoto({ base, media }) {
897
+ function PrivatePhoto({ base, media, onChange, disabled }) {
851
898
  const [url, setUrl] = react.useState("");
852
899
  const [video, setVideo] = react.useState(false);
853
900
  react.useEffect(() => {
@@ -881,9 +928,12 @@ function PrivatePhoto({ base, media }) {
881
928
  "aria-label": media.alt
882
929
  }
883
930
  ) : /* @__PURE__ */ jsxRuntime.jsx("img", { src: url, alt: media.alt, referrerPolicy: "no-referrer" }) : /* @__PURE__ */ jsxRuntime.jsx("p", { children: "Media unavailable." }),
884
- /* @__PURE__ */ jsxRuntime.jsxs("figcaption", { children: [
885
- media.caption,
886
- media.credit && ` Photo: ${media.credit}`
931
+ /* @__PURE__ */ jsxRuntime.jsxs("figcaption", { className: "sm-review-photo-details", children: [
932
+ (media.caption || media.credit) && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
933
+ media.caption,
934
+ media.credit && ` Photo: ${media.credit}`
935
+ ] }),
936
+ onChange && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "sm-review-change-photo", type: "button", disabled, onClick: onChange, children: "Change photo" })
887
937
  ] })
888
938
  ] });
889
939
  }
@@ -75,6 +75,13 @@ function StoryReview({
75
75
  const thanksRef = useRef(null);
76
76
  const [moreQuestions, setMoreQuestions] = useState(false);
77
77
  const [correctionKind, setCorrectionKind] = useState("Correct a detail");
78
+ const [replacementPhoto, setReplacementPhoto] = useState(null);
79
+ function openMedia(photo = null) {
80
+ setReplacementPhoto(photo);
81
+ setFile(null);
82
+ setRights(false);
83
+ setPanel("media");
84
+ }
78
85
  useEffect(() => {
79
86
  if (panel !== "article") {
80
87
  responseRef.current?.scrollIntoView({
@@ -195,13 +202,34 @@ function StoryReview({
195
202
  "metadata",
196
203
  JSON.stringify({ ...mediaInfo, rights_confirmed: true })
197
204
  );
198
- await request("/media", form);
205
+ const received = await request("/media", form);
206
+ let uploadNotice = "Media received privately. The newsroom will review it before publication.";
207
+ if (replacementPhoto) {
208
+ uploadNotice = "Your replacement photo has been sent to the newsroom. The current photo will stay in this preview until we prepare an updated draft.";
209
+ if (can("COMMENT")) {
210
+ try {
211
+ await request("", {
212
+ revision_id: review.revision.id,
213
+ action: "COMMENT",
214
+ message: "Please use my newly uploaded photograph in place of this photo.",
215
+ target: {
216
+ kind: "PHOTO",
217
+ reference: replacementPhoto.contribution_id,
218
+ replacement_contribution_id: received.id
219
+ }
220
+ });
221
+ } catch {
222
+ uploadNotice = "Your photo was uploaded privately, but we couldn't attach the replacement note. Please use Suggest a Change to tell the newsroom which photograph it should replace.";
223
+ }
224
+ } else {
225
+ uploadNotice = "Your photo was uploaded privately for newsroom review. The newsroom will select the photo for the next preview.";
226
+ }
227
+ }
199
228
  await load();
200
229
  setFile(null);
201
230
  setRights(false);
202
- setNotice(
203
- "Media received privately. The newsroom will review it before publication."
204
- );
231
+ setNotice(uploadNotice);
232
+ setReplacementPhoto(null);
205
233
  setPanel("article");
206
234
  } catch (e) {
207
235
  setError(e instanceof Error ? e.message : "Upload failed.");
@@ -332,7 +360,16 @@ function StoryReview({
332
360
  /* @__PURE__ */ jsx("h1", { children: review.revision.document.title }),
333
361
  /* @__PURE__ */ jsx("p", { className: "sm-review-dek", children: review.revision.document.subtitle }),
334
362
  /* @__PURE__ */ jsx(EditorialNotices, { editorial: review.revision.editorial }),
335
- review.revision.document.media.filter((m) => m.position === "hero").map((m) => /* @__PURE__ */ jsx(PrivatePhoto, { media: m, base }, m.file_id)),
363
+ review.revision.document.media.filter((m) => m.position === "hero").map((m) => /* @__PURE__ */ jsx(
364
+ PrivatePhoto,
365
+ {
366
+ media: m,
367
+ base,
368
+ onChange: can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
369
+ disabled: busy
370
+ },
371
+ m.file_id
372
+ )),
336
373
  celebration && !review.revision.document.media.some(
337
374
  (m) => m.position === "hero"
338
375
  ) && can("UPLOAD_MEDIA") && /* @__PURE__ */ jsxs(
@@ -345,7 +382,7 @@ function StoryReview({
345
382
  /* @__PURE__ */ jsx("strong", { children: "A photograph makes it yours." }),
346
383
  /* @__PURE__ */ jsx("p", { children: "Family photograph may be added before publication." })
347
384
  ] }),
348
- /* @__PURE__ */ jsx("button", { onClick: () => setPanel("media"), children: "+ Add a photograph" })
385
+ /* @__PURE__ */ jsx("button", { onClick: () => openMedia(), children: "+ Add a photograph" })
349
386
  ]
350
387
  }
351
388
  ),
@@ -356,7 +393,16 @@ function StoryReview({
356
393
  dangerouslySetInnerHTML: { __html: review.revision.content_html }
357
394
  }
358
395
  ),
359
- review.revision.document.media.filter((m) => m.position !== "hero").map((m) => /* @__PURE__ */ jsx(PrivatePhoto, { media: m, base }, m.file_id))
396
+ review.revision.document.media.filter((m) => m.position !== "hero").map((m) => /* @__PURE__ */ jsx(
397
+ PrivatePhoto,
398
+ {
399
+ media: m,
400
+ base,
401
+ onChange: m.kind !== "video" && can("UPLOAD_MEDIA") ? () => openMedia(m) : void 0,
402
+ disabled: busy
403
+ },
404
+ m.file_id
405
+ ))
360
406
  ] }),
361
407
  celebration && /* @__PURE__ */ jsxs("section", { id: "review-options", className: "sm-review-welcome", children: [
362
408
  /* @__PURE__ */ jsx("h2", { children: "Help us make this just right" }),
@@ -399,7 +445,7 @@ function StoryReview({
399
445
  ),
400
446
  can("REQUEST_CHANGES") && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("changes"), children: celebration ? "Suggest a Change" : "Request changes" }),
401
447
  celebration && can("PROPOSE_EDITS") && !can("REQUEST_CHANGES") && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("edits"), children: "Suggest a Change" }),
402
- can("UPLOAD_MEDIA") && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("media"), children: celebration ? "Add Photos" : "Add photos" }),
448
+ can("UPLOAD_MEDIA") && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => openMedia(), children: celebration ? "Add Photos" : "Add photos" }),
403
449
  !celebration && /* @__PURE__ */ jsxs(Fragment, { children: [
404
450
  can("PROPOSE_EDITS") && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("edits"), children: "Suggest edits" }),
405
451
  can("ANSWER_QUESTIONS") && review.questions.length > 0 && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("questions"), children: "Answer questions" }),
@@ -421,7 +467,7 @@ function StoryReview({
421
467
  "aria-label": "Your response",
422
468
  children: [
423
469
  /* @__PURE__ */ jsxs("div", { className: "sm-review-form-heading", children: [
424
- /* @__PURE__ */ jsx("h2", { children: panel === "approve" ? "Approve this exact draft" : panel === "edits" ? "Propose changes" : panel === "media" ? "Contribute a photograph" : panel === "questions" ? "Tell us a little more" : panel === "history" ? "Selected revision history" : panel === "decline" ? "Prefer not to publish" : panel === "changes" ? "Suggest a change" : "Message the newsroom" }),
470
+ /* @__PURE__ */ jsx("h2", { children: panel === "approve" ? "Approve this exact draft" : panel === "edits" ? "Propose changes" : panel === "media" ? replacementPhoto ? "Change photo" : "Contribute a photograph" : panel === "questions" ? "Tell us a little more" : panel === "history" ? "Selected revision history" : panel === "decline" ? "Prefer not to publish" : panel === "changes" ? "Suggest a change" : "Message the newsroom" }),
425
471
  /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Close" })
426
472
  ] }),
427
473
  panel === "edits" && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -560,6 +606,7 @@ function StoryReview({
560
606
  void upload();
561
607
  },
562
608
  children: [
609
+ replacementPhoto && /* @__PURE__ */ jsx("p", { children: "Choose the photograph you'd like us to use instead. We'll review it and prepare an updated preview; the current photo stays in place until then." }),
563
610
  /* @__PURE__ */ jsx("p", { children: "JPEG, PNG or WebP photos up to 12 MB; MP4 or WebM videos up to 25 MB. Media remain private until selected and approved by the newsroom." }),
564
611
  /* @__PURE__ */ jsxs("label", { children: [
565
612
  "Photograph or video",
@@ -845,7 +892,7 @@ function StoryReview({
845
892
  publicationFooter
846
893
  ] });
847
894
  }
848
- function PrivatePhoto({ base, media }) {
895
+ function PrivatePhoto({ base, media, onChange, disabled }) {
849
896
  const [url, setUrl] = useState("");
850
897
  const [video, setVideo] = useState(false);
851
898
  useEffect(() => {
@@ -879,9 +926,12 @@ function PrivatePhoto({ base, media }) {
879
926
  "aria-label": media.alt
880
927
  }
881
928
  ) : /* @__PURE__ */ jsx("img", { src: url, alt: media.alt, referrerPolicy: "no-referrer" }) : /* @__PURE__ */ jsx("p", { children: "Media unavailable." }),
882
- /* @__PURE__ */ jsxs("figcaption", { children: [
883
- media.caption,
884
- media.credit && ` Photo: ${media.credit}`
929
+ /* @__PURE__ */ jsxs("figcaption", { className: "sm-review-photo-details", children: [
930
+ (media.caption || media.credit) && /* @__PURE__ */ jsxs("span", { children: [
931
+ media.caption,
932
+ media.credit && ` Photo: ${media.credit}`
933
+ ] }),
934
+ onChange && /* @__PURE__ */ jsx("button", { className: "sm-review-change-photo", type: "button", disabled, onClick: onChange, children: "Change photo" })
885
935
  ] })
886
936
  ] });
887
937
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/news-publication",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
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",
package/story-review.css CHANGED
@@ -151,6 +151,24 @@
151
151
  color: #496076;
152
152
  margin-top: 8px;
153
153
  }
154
+ .sm-review-photo-details {
155
+ display: flex;
156
+ align-items: baseline;
157
+ justify-content: space-between;
158
+ flex-wrap: wrap;
159
+ gap: 8px 16px;
160
+ margin-top: 10px;
161
+ }
162
+ .sm-review-photo-details > span {
163
+ flex: 1 1 200px;
164
+ margin: 0;
165
+ color: var(--pub-muted, #545c55);
166
+ }
167
+ .sm-story-review .sm-review-change-photo {
168
+ margin-left: auto;
169
+ min-height: 44px;
170
+ font-size: 13px;
171
+ }
154
172
  .sm-review-actions {
155
173
  position: sticky;
156
174
  bottom: 0;
@@ -565,6 +583,7 @@
565
583
  }
566
584
  }
567
585
  @media print {
586
+ .sm-review-change-photo,
568
587
  .sm-review-concierge-actions,
569
588
  .sm-review-welcome,
570
589
  .sm-review-utilities,