@scalemule/news-publication 0.0.30 → 0.0.32
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/dist/story-review.cjs +181 -11
- package/dist/story-review.js +181 -11
- package/package.json +1 -1
- package/story-review.css +7 -0
package/dist/story-review.cjs
CHANGED
|
@@ -75,7 +75,11 @@ function useReviewDrafts(session, revision, initial, request) {
|
|
|
75
75
|
const flushRef = react.useRef(flush);
|
|
76
76
|
flushRef.current = flush;
|
|
77
77
|
react.useEffect(() => {
|
|
78
|
-
if (!revision)
|
|
78
|
+
if (!revision) {
|
|
79
|
+
entries.current = {};
|
|
80
|
+
redraw();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
79
83
|
const id = `${session}:${revision}`;
|
|
80
84
|
active.current = id;
|
|
81
85
|
const next = {};
|
|
@@ -292,12 +296,29 @@ function StoryReview({
|
|
|
292
296
|
publicationFooter
|
|
293
297
|
}) {
|
|
294
298
|
const base = `/api/news/review/${encodeURIComponent(sessionId)}`;
|
|
295
|
-
const [review, setReview] = react.useState(null), [error, setError] = react.useState(""), [notice, setNotice] = react.useState(""), [busy, setBusy] = react.useState(false), [panel,
|
|
299
|
+
const [review, setReview] = react.useState(null), [error, setError] = react.useState(""), [notice, setNotice] = react.useState(""), [busy, setBusy] = react.useState(false), [panel, setPanelState] = react.useState("article"), [verification, setVerification] = react.useState(false), [code, setCode] = react.useState("");
|
|
300
|
+
const [shareUrl, setShareUrl] = react.useState(""), [canShare, setCanShare] = react.useState(false);
|
|
301
|
+
const [contributorName, setContributorName] = react.useState(""), [contributorEmail, setContributorEmail] = react.useState("");
|
|
302
|
+
const [identityStep, setIdentityStep] = react.useState("name"), [requestedPanel, setRequestedPanel] = react.useState("comment");
|
|
303
|
+
function setPanel(next) {
|
|
304
|
+
if (review?.identity_required && !["article", "history", "identity", "share"].includes(next)) {
|
|
305
|
+
setRequestedPanel(next);
|
|
306
|
+
setPanelState("identity");
|
|
307
|
+
} else setPanelState(next);
|
|
308
|
+
}
|
|
309
|
+
const identity = review?.session_id || sessionId;
|
|
296
310
|
const [rights, setRights] = react.useState(false), [compare, setCompare] = react.useState(""), [compareTo, setCompareTo] = react.useState(""), [editingMedia, setEditingMedia] = react.useState(""), [commentTarget, setCommentTarget] = react.useState("STORY");
|
|
297
|
-
const pendingPhoto = usePendingPhoto(review ? `${
|
|
311
|
+
const pendingPhoto = usePendingPhoto(review && !review.identity_required ? `${identity}:${review.revision.id}` : void 0);
|
|
298
312
|
const file = pendingPhoto.photo?.file || null;
|
|
299
313
|
const galleryRef = react.useRef(null);
|
|
300
314
|
const cameraRef = react.useRef(null);
|
|
315
|
+
const errorRef = react.useRef(null);
|
|
316
|
+
react.useEffect(() => {
|
|
317
|
+
if (error) {
|
|
318
|
+
errorRef.current?.scrollIntoView({ block: "center" });
|
|
319
|
+
errorRef.current?.focus({ preventScroll: true });
|
|
320
|
+
}
|
|
321
|
+
}, [error]);
|
|
301
322
|
const noticeRef = react.useRef(null);
|
|
302
323
|
react.useEffect(() => {
|
|
303
324
|
if (notice) {
|
|
@@ -329,7 +350,7 @@ function StoryReview({
|
|
|
329
350
|
}
|
|
330
351
|
}, [panel]);
|
|
331
352
|
const emptyMediaInfo = { caption: "", who_is_pictured: "", creator: "", when_taken: "", where_taken: "", credit: "", alt: "" };
|
|
332
|
-
const drafts = useReviewDrafts(
|
|
353
|
+
const drafts = useReviewDrafts(identity, review?.identity_required ? void 0 : review?.revision.id, review?.response_drafts, request);
|
|
333
354
|
const currentDraft = drafts.entries[panel];
|
|
334
355
|
const message = currentDraft?.data.message || "";
|
|
335
356
|
const answers = drafts.entries.questions?.data.answers || {};
|
|
@@ -382,10 +403,23 @@ function StoryReview({
|
|
|
382
403
|
let active = true;
|
|
383
404
|
void (async () => {
|
|
384
405
|
try {
|
|
385
|
-
const token = window.location.hash.slice(1);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
406
|
+
const token = new URL(window.location.href).searchParams.get("key") || (/^[a-f0-9]{64}$/.test(window.location.hash.slice(1)) ? window.location.hash.slice(1) : void 0);
|
|
407
|
+
let exchange = {};
|
|
408
|
+
try {
|
|
409
|
+
exchange = await request("/session", token ? { token } : {});
|
|
410
|
+
} catch (e) {
|
|
411
|
+
if (token) throw e;
|
|
412
|
+
}
|
|
413
|
+
const address = exchange.share_url || (token ? `/review/${encodeURIComponent(sessionId)}?key=${encodeURIComponent(token)}` : void 0);
|
|
414
|
+
if (address) {
|
|
415
|
+
const canonical = new URL(address, window.location.origin);
|
|
416
|
+
if (canonical.origin === window.location.origin) {
|
|
417
|
+
window.history.replaceState(null, "", canonical.pathname + canonical.search);
|
|
418
|
+
if (active) {
|
|
419
|
+
setShareUrl(canonical.href);
|
|
420
|
+
setCanShare(exchange.can_share === true);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
389
423
|
}
|
|
390
424
|
const data = await request();
|
|
391
425
|
if (active) {
|
|
@@ -406,6 +440,18 @@ function StoryReview({
|
|
|
406
440
|
active = false;
|
|
407
441
|
};
|
|
408
442
|
}, [sessionId]);
|
|
443
|
+
async function sharePrivateLink() {
|
|
444
|
+
if (!shareUrl) return;
|
|
445
|
+
if (navigator.share) {
|
|
446
|
+
try {
|
|
447
|
+
await navigator.share({ title: "Private story preview", url: shareUrl });
|
|
448
|
+
return;
|
|
449
|
+
} catch (e) {
|
|
450
|
+
if (e.name === "AbortError") return;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
setPanel("share");
|
|
454
|
+
}
|
|
409
455
|
async function send(action, success) {
|
|
410
456
|
if (!review || busy) return;
|
|
411
457
|
if (action.action === "APPROVE" && file) {
|
|
@@ -532,6 +578,7 @@ function StoryReview({
|
|
|
532
578
|
const selected = revisions.find((r) => r.id === compare), selectedAfter = revisions.find(
|
|
533
579
|
(r) => r.id === (compareTo || review?.revision.id)
|
|
534
580
|
);
|
|
581
|
+
const errorMessage = error ? /* @__PURE__ */ jsxRuntime.jsx("p", { ref: errorRef, tabIndex: -1, role: "alert", className: "sm-review-alert", children: error }) : null;
|
|
535
582
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `sm-review-frame${publicationHeader ? " sm-review-publication-frame" : ""}`, children: [
|
|
536
583
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sm-review-banner-wrap", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-private", children: [
|
|
537
584
|
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: review?.published ? "Private review" : "Private preview \xB7 Not published" }),
|
|
@@ -542,7 +589,7 @@ function StoryReview({
|
|
|
542
589
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: review?.publication.name || publicationName })
|
|
543
590
|
] }) }),
|
|
544
591
|
/* @__PURE__ */ jsxRuntime.jsxs("main", { className: `sm-story-review${celebration ? " sm-review-celebration" : ""}`, children: [
|
|
545
|
-
|
|
592
|
+
(!review || panel === "article" || verification) && errorMessage,
|
|
546
593
|
notice && !(approved && finalEditor && notice === approvalThanks) && /* @__PURE__ */ jsxRuntime.jsx("p", { ref: noticeRef, tabIndex: -1, role: "status", className: "sm-review-notice", children: notice }),
|
|
547
594
|
verification && /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "sm-review-form", children: [
|
|
548
595
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Verify your email" }),
|
|
@@ -610,8 +657,36 @@ function StoryReview({
|
|
|
610
657
|
}
|
|
611
658
|
)
|
|
612
659
|
] }),
|
|
660
|
+
!review && error && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
661
|
+
"Ask the person who invited you to use ",
|
|
662
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Share private preview" }),
|
|
663
|
+
" or copy the complete address from their open preview. You do not need an account to read it."
|
|
664
|
+
] }),
|
|
613
665
|
!review && !error && !verification && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Opening your private preview\u2026" }),
|
|
614
666
|
review && document && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
667
|
+
canShare && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-sharing", children: [
|
|
668
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Private family preview. Share only with people you want to invite." }),
|
|
669
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => void sharePrivateLink(), children: "Share private preview" })
|
|
670
|
+
] }),
|
|
671
|
+
review.participant && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-identity-status", children: [
|
|
672
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
673
|
+
"Contributing as ",
|
|
674
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: review.reviewer })
|
|
675
|
+
] }),
|
|
676
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy || pending.length > 0 || !!file, onClick: async () => {
|
|
677
|
+
setBusy(true);
|
|
678
|
+
try {
|
|
679
|
+
await request("/leave", {});
|
|
680
|
+
setPanelState("article");
|
|
681
|
+
setIdentityStep("name");
|
|
682
|
+
await load();
|
|
683
|
+
} catch (e) {
|
|
684
|
+
setError(e.message);
|
|
685
|
+
} finally {
|
|
686
|
+
setBusy(false);
|
|
687
|
+
}
|
|
688
|
+
}, children: "Not you?" })
|
|
689
|
+
] }),
|
|
615
690
|
celebration ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-intro", children: [
|
|
616
691
|
review.revision.number > 1 ? /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
617
692
|
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Updated preview" }),
|
|
@@ -694,7 +769,10 @@ function StoryReview({
|
|
|
694
769
|
] }),
|
|
695
770
|
celebration && /* @__PURE__ */ jsxRuntime.jsxs("section", { id: "review-options", className: "sm-review-welcome", children: [
|
|
696
771
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Help us make this just right" }),
|
|
697
|
-
/* @__PURE__ */ jsxRuntime.
|
|
772
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
773
|
+
"We prepared this announcement as a starting point so you don't have to write anything yourself. ",
|
|
774
|
+
can("APPROVE") ? "If everything looks right, you can simply approve it." : "Read it at your own pace, and let us know if anything needs a change."
|
|
775
|
+
] }),
|
|
698
776
|
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "If you'd like, you can also correct a detail, add photographs, or tell us a little more about the couple. We'll take care of turning any additional information into the next draft." }),
|
|
699
777
|
can("ANSWER_QUESTIONS") && review.questions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
700
778
|
"button",
|
|
@@ -722,6 +800,7 @@ function StoryReview({
|
|
|
722
800
|
className: `sm-review-actions${celebration ? " sm-review-concierge-actions" : ""}${panel !== "article" ? " sm-review-actions-editing" : ""}`,
|
|
723
801
|
"aria-label": "Review actions",
|
|
724
802
|
children: [
|
|
803
|
+
!can("APPROVE") && can("COMMENT") && (review.identity_required || review.participant) && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("feedback"), children: "Looks good to me" }),
|
|
725
804
|
can("APPROVE") && !review.published && /* @__PURE__ */ jsxRuntime.jsx(
|
|
726
805
|
"button",
|
|
727
806
|
{
|
|
@@ -755,7 +834,97 @@ function StoryReview({
|
|
|
755
834
|
] }, name)),
|
|
756
835
|
file && !pending.some(([name]) => name === "media") && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("media"), children: "Continue your photo upload" })
|
|
757
836
|
] }),
|
|
758
|
-
panel
|
|
837
|
+
panel === "share" && /* @__PURE__ */ jsxRuntime.jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Share private preview", children: [
|
|
838
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Share with your family" }),
|
|
839
|
+
errorMessage,
|
|
840
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Anyone with this link can read the private preview. To send feedback or photos, they will enter their own name and verify their email. No account is needed." }),
|
|
841
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "review-share-url", children: "Private preview link" }),
|
|
842
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { id: "review-share-url", readOnly: true, value: shareUrl, onFocus: (e) => e.currentTarget.select() }),
|
|
843
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: async () => {
|
|
844
|
+
try {
|
|
845
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
846
|
+
setNotice("Private link copied. Paste it into your message to family.");
|
|
847
|
+
} catch {
|
|
848
|
+
setNotice("Select the link above, then choose Copy on your device.");
|
|
849
|
+
}
|
|
850
|
+
}, children: "Copy private link" }),
|
|
851
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("article"), children: "Back to the story" })
|
|
852
|
+
] }),
|
|
853
|
+
panel === "identity" && /* @__PURE__ */ jsxRuntime.jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Introduce yourself", children: [
|
|
854
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: identityStep === "code" ? "Check your email" : "Who is helping with the story?" }),
|
|
855
|
+
errorMessage,
|
|
856
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Your name will accompany your feedback for the newsroom. Your email stays private. No password or account is needed." }),
|
|
857
|
+
identityStep === "name" ? /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: async (e) => {
|
|
858
|
+
e.preventDefault();
|
|
859
|
+
setBusy(true);
|
|
860
|
+
setError("");
|
|
861
|
+
try {
|
|
862
|
+
await request("/join", { name: contributorName, email: contributorEmail });
|
|
863
|
+
await request("/verify", {});
|
|
864
|
+
setIdentityStep("code");
|
|
865
|
+
setCode("");
|
|
866
|
+
} catch (e2) {
|
|
867
|
+
setError(e2.message);
|
|
868
|
+
} finally {
|
|
869
|
+
setBusy(false);
|
|
870
|
+
}
|
|
871
|
+
}, children: [
|
|
872
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "contributor-name", children: "Your name" }),
|
|
873
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { id: "contributor-name", autoComplete: "name", value: contributorName, maxLength: 150, required: true, onChange: (e) => setContributorName(e.target.value) }),
|
|
874
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "contributor-email", children: "Your email" }),
|
|
875
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { id: "contributor-email", type: "email", autoComplete: "email", value: contributorEmail, maxLength: 320, required: true, onChange: (e) => setContributorEmail(e.target.value) }),
|
|
876
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "We'll email you a code to confirm it's you." }),
|
|
877
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, children: busy ? "Sending your code\u2026" : "Email me a code" })
|
|
878
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: async (e) => {
|
|
879
|
+
e.preventDefault();
|
|
880
|
+
setBusy(true);
|
|
881
|
+
setError("");
|
|
882
|
+
try {
|
|
883
|
+
await request("/verify", { code });
|
|
884
|
+
await load();
|
|
885
|
+
setCode("");
|
|
886
|
+
setPanelState(requestedPanel);
|
|
887
|
+
} catch (e2) {
|
|
888
|
+
setError(e2.message);
|
|
889
|
+
} finally {
|
|
890
|
+
setBusy(false);
|
|
891
|
+
}
|
|
892
|
+
}, children: [
|
|
893
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
894
|
+
"Enter the eight-digit code sent to ",
|
|
895
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: contributorEmail }),
|
|
896
|
+
". It expires in ten minutes."
|
|
897
|
+
] }),
|
|
898
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "contributor-code", children: "Email verification code" }),
|
|
899
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { id: "contributor-code", inputMode: "numeric", autoComplete: "one-time-code", pattern: "[0-9]{8}", maxLength: 8, value: code, required: true, onChange: (e) => setCode(e.target.value.replace(/\s/g, "")) }),
|
|
900
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, children: busy ? "Verifying\u2026" : "Verify and continue" }),
|
|
901
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", disabled: busy, onClick: async () => {
|
|
902
|
+
setBusy(true);
|
|
903
|
+
try {
|
|
904
|
+
await request("/verify", {});
|
|
905
|
+
setNotice("A new code was sent. Check your inbox and spam folder.");
|
|
906
|
+
} catch (e) {
|
|
907
|
+
setError(e.message);
|
|
908
|
+
} finally {
|
|
909
|
+
setBusy(false);
|
|
910
|
+
}
|
|
911
|
+
}, children: "Send a new code" }),
|
|
912
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", disabled: busy, onClick: () => setIdentityStep("name"), children: "Use a different email" })
|
|
913
|
+
] }),
|
|
914
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Just read the story" })
|
|
915
|
+
] }),
|
|
916
|
+
panel === "feedback" && /* @__PURE__ */ jsxRuntime.jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Family feedback", children: [
|
|
917
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Looks good to you?" }),
|
|
918
|
+
errorMessage,
|
|
919
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
920
|
+
"We'll send your feedback as ",
|
|
921
|
+
review.reviewer,
|
|
922
|
+
". The newsroom will request approval from the designated family reviewer separately. This will not publish the story."
|
|
923
|
+
] }),
|
|
924
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => void send({ action: "COMMENT", message: "This draft looks good to me.", target: { kind: "STORY" } }, "Thank you. Your feedback has been sent to the newsroom. The story is still private."), children: "Send my feedback" }),
|
|
925
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Back to the story" })
|
|
926
|
+
] }),
|
|
927
|
+
!["article", "identity", "share", "feedback"].includes(panel) && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
759
928
|
"section",
|
|
760
929
|
{
|
|
761
930
|
ref: responseRef,
|
|
@@ -767,6 +936,7 @@ function StoryReview({
|
|
|
767
936
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: panel === "approve" ? "Approve this exact draft" : panel === "edits" ? "Propose changes" : panel === "media" ? photoToReplace ? "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" }),
|
|
768
937
|
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Close" })
|
|
769
938
|
] }),
|
|
939
|
+
errorMessage,
|
|
770
940
|
panel !== "history" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-save-state", role: "status", "aria-live": "polite", children: [
|
|
771
941
|
currentDraft?.status === "conflict" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
772
942
|
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "A response was saved in another tab or device. Your writing here is still available. Choose which version to keep." }),
|
package/dist/story-review.js
CHANGED
|
@@ -73,7 +73,11 @@ function useReviewDrafts(session, revision, initial, request) {
|
|
|
73
73
|
const flushRef = useRef(flush);
|
|
74
74
|
flushRef.current = flush;
|
|
75
75
|
useEffect(() => {
|
|
76
|
-
if (!revision)
|
|
76
|
+
if (!revision) {
|
|
77
|
+
entries.current = {};
|
|
78
|
+
redraw();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
77
81
|
const id = `${session}:${revision}`;
|
|
78
82
|
active.current = id;
|
|
79
83
|
const next = {};
|
|
@@ -290,12 +294,29 @@ function StoryReview({
|
|
|
290
294
|
publicationFooter
|
|
291
295
|
}) {
|
|
292
296
|
const base = `/api/news/review/${encodeURIComponent(sessionId)}`;
|
|
293
|
-
const [review, setReview] = useState(null), [error, setError] = useState(""), [notice, setNotice] = useState(""), [busy, setBusy] = useState(false), [panel,
|
|
297
|
+
const [review, setReview] = useState(null), [error, setError] = useState(""), [notice, setNotice] = useState(""), [busy, setBusy] = useState(false), [panel, setPanelState] = useState("article"), [verification, setVerification] = useState(false), [code, setCode] = useState("");
|
|
298
|
+
const [shareUrl, setShareUrl] = useState(""), [canShare, setCanShare] = useState(false);
|
|
299
|
+
const [contributorName, setContributorName] = useState(""), [contributorEmail, setContributorEmail] = useState("");
|
|
300
|
+
const [identityStep, setIdentityStep] = useState("name"), [requestedPanel, setRequestedPanel] = useState("comment");
|
|
301
|
+
function setPanel(next) {
|
|
302
|
+
if (review?.identity_required && !["article", "history", "identity", "share"].includes(next)) {
|
|
303
|
+
setRequestedPanel(next);
|
|
304
|
+
setPanelState("identity");
|
|
305
|
+
} else setPanelState(next);
|
|
306
|
+
}
|
|
307
|
+
const identity = review?.session_id || sessionId;
|
|
294
308
|
const [rights, setRights] = useState(false), [compare, setCompare] = useState(""), [compareTo, setCompareTo] = useState(""), [editingMedia, setEditingMedia] = useState(""), [commentTarget, setCommentTarget] = useState("STORY");
|
|
295
|
-
const pendingPhoto = usePendingPhoto(review ? `${
|
|
309
|
+
const pendingPhoto = usePendingPhoto(review && !review.identity_required ? `${identity}:${review.revision.id}` : void 0);
|
|
296
310
|
const file = pendingPhoto.photo?.file || null;
|
|
297
311
|
const galleryRef = useRef(null);
|
|
298
312
|
const cameraRef = useRef(null);
|
|
313
|
+
const errorRef = useRef(null);
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (error) {
|
|
316
|
+
errorRef.current?.scrollIntoView({ block: "center" });
|
|
317
|
+
errorRef.current?.focus({ preventScroll: true });
|
|
318
|
+
}
|
|
319
|
+
}, [error]);
|
|
299
320
|
const noticeRef = useRef(null);
|
|
300
321
|
useEffect(() => {
|
|
301
322
|
if (notice) {
|
|
@@ -327,7 +348,7 @@ function StoryReview({
|
|
|
327
348
|
}
|
|
328
349
|
}, [panel]);
|
|
329
350
|
const emptyMediaInfo = { caption: "", who_is_pictured: "", creator: "", when_taken: "", where_taken: "", credit: "", alt: "" };
|
|
330
|
-
const drafts = useReviewDrafts(
|
|
351
|
+
const drafts = useReviewDrafts(identity, review?.identity_required ? void 0 : review?.revision.id, review?.response_drafts, request);
|
|
331
352
|
const currentDraft = drafts.entries[panel];
|
|
332
353
|
const message = currentDraft?.data.message || "";
|
|
333
354
|
const answers = drafts.entries.questions?.data.answers || {};
|
|
@@ -380,10 +401,23 @@ function StoryReview({
|
|
|
380
401
|
let active = true;
|
|
381
402
|
void (async () => {
|
|
382
403
|
try {
|
|
383
|
-
const token = window.location.hash.slice(1);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
404
|
+
const token = new URL(window.location.href).searchParams.get("key") || (/^[a-f0-9]{64}$/.test(window.location.hash.slice(1)) ? window.location.hash.slice(1) : void 0);
|
|
405
|
+
let exchange = {};
|
|
406
|
+
try {
|
|
407
|
+
exchange = await request("/session", token ? { token } : {});
|
|
408
|
+
} catch (e) {
|
|
409
|
+
if (token) throw e;
|
|
410
|
+
}
|
|
411
|
+
const address = exchange.share_url || (token ? `/review/${encodeURIComponent(sessionId)}?key=${encodeURIComponent(token)}` : void 0);
|
|
412
|
+
if (address) {
|
|
413
|
+
const canonical = new URL(address, window.location.origin);
|
|
414
|
+
if (canonical.origin === window.location.origin) {
|
|
415
|
+
window.history.replaceState(null, "", canonical.pathname + canonical.search);
|
|
416
|
+
if (active) {
|
|
417
|
+
setShareUrl(canonical.href);
|
|
418
|
+
setCanShare(exchange.can_share === true);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
387
421
|
}
|
|
388
422
|
const data = await request();
|
|
389
423
|
if (active) {
|
|
@@ -404,6 +438,18 @@ function StoryReview({
|
|
|
404
438
|
active = false;
|
|
405
439
|
};
|
|
406
440
|
}, [sessionId]);
|
|
441
|
+
async function sharePrivateLink() {
|
|
442
|
+
if (!shareUrl) return;
|
|
443
|
+
if (navigator.share) {
|
|
444
|
+
try {
|
|
445
|
+
await navigator.share({ title: "Private story preview", url: shareUrl });
|
|
446
|
+
return;
|
|
447
|
+
} catch (e) {
|
|
448
|
+
if (e.name === "AbortError") return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
setPanel("share");
|
|
452
|
+
}
|
|
407
453
|
async function send(action, success) {
|
|
408
454
|
if (!review || busy) return;
|
|
409
455
|
if (action.action === "APPROVE" && file) {
|
|
@@ -530,6 +576,7 @@ function StoryReview({
|
|
|
530
576
|
const selected = revisions.find((r) => r.id === compare), selectedAfter = revisions.find(
|
|
531
577
|
(r) => r.id === (compareTo || review?.revision.id)
|
|
532
578
|
);
|
|
579
|
+
const errorMessage = error ? /* @__PURE__ */ jsx("p", { ref: errorRef, tabIndex: -1, role: "alert", className: "sm-review-alert", children: error }) : null;
|
|
533
580
|
return /* @__PURE__ */ jsxs("div", { className: `sm-review-frame${publicationHeader ? " sm-review-publication-frame" : ""}`, children: [
|
|
534
581
|
/* @__PURE__ */ jsx("div", { className: "sm-review-banner-wrap", children: /* @__PURE__ */ jsxs("div", { className: "sm-review-private", children: [
|
|
535
582
|
/* @__PURE__ */ jsx("strong", { children: review?.published ? "Private review" : "Private preview \xB7 Not published" }),
|
|
@@ -540,7 +587,7 @@ function StoryReview({
|
|
|
540
587
|
/* @__PURE__ */ jsx("span", { children: review?.publication.name || publicationName })
|
|
541
588
|
] }) }),
|
|
542
589
|
/* @__PURE__ */ jsxs("main", { className: `sm-story-review${celebration ? " sm-review-celebration" : ""}`, children: [
|
|
543
|
-
|
|
590
|
+
(!review || panel === "article" || verification) && errorMessage,
|
|
544
591
|
notice && !(approved && finalEditor && notice === approvalThanks) && /* @__PURE__ */ jsx("p", { ref: noticeRef, tabIndex: -1, role: "status", className: "sm-review-notice", children: notice }),
|
|
545
592
|
verification && /* @__PURE__ */ jsxs("section", { className: "sm-review-form", children: [
|
|
546
593
|
/* @__PURE__ */ jsx("h2", { children: "Verify your email" }),
|
|
@@ -608,8 +655,36 @@ function StoryReview({
|
|
|
608
655
|
}
|
|
609
656
|
)
|
|
610
657
|
] }),
|
|
658
|
+
!review && error && /* @__PURE__ */ jsxs("p", { children: [
|
|
659
|
+
"Ask the person who invited you to use ",
|
|
660
|
+
/* @__PURE__ */ jsx("strong", { children: "Share private preview" }),
|
|
661
|
+
" or copy the complete address from their open preview. You do not need an account to read it."
|
|
662
|
+
] }),
|
|
611
663
|
!review && !error && !verification && /* @__PURE__ */ jsx("p", { role: "status", children: "Opening your private preview\u2026" }),
|
|
612
664
|
review && document && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
665
|
+
canShare && /* @__PURE__ */ jsxs("div", { className: "sm-review-sharing", children: [
|
|
666
|
+
/* @__PURE__ */ jsx("p", { children: "Private family preview. Share only with people you want to invite." }),
|
|
667
|
+
/* @__PURE__ */ jsx("button", { onClick: () => void sharePrivateLink(), children: "Share private preview" })
|
|
668
|
+
] }),
|
|
669
|
+
review.participant && /* @__PURE__ */ jsxs("div", { className: "sm-review-identity-status", children: [
|
|
670
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
671
|
+
"Contributing as ",
|
|
672
|
+
/* @__PURE__ */ jsx("strong", { children: review.reviewer })
|
|
673
|
+
] }),
|
|
674
|
+
/* @__PURE__ */ jsx("button", { disabled: busy || pending.length > 0 || !!file, onClick: async () => {
|
|
675
|
+
setBusy(true);
|
|
676
|
+
try {
|
|
677
|
+
await request("/leave", {});
|
|
678
|
+
setPanelState("article");
|
|
679
|
+
setIdentityStep("name");
|
|
680
|
+
await load();
|
|
681
|
+
} catch (e) {
|
|
682
|
+
setError(e.message);
|
|
683
|
+
} finally {
|
|
684
|
+
setBusy(false);
|
|
685
|
+
}
|
|
686
|
+
}, children: "Not you?" })
|
|
687
|
+
] }),
|
|
613
688
|
celebration ? /* @__PURE__ */ jsxs("div", { className: "sm-review-intro", children: [
|
|
614
689
|
review.revision.number > 1 ? /* @__PURE__ */ jsxs("p", { children: [
|
|
615
690
|
/* @__PURE__ */ jsx("strong", { children: "Updated preview" }),
|
|
@@ -692,7 +767,10 @@ function StoryReview({
|
|
|
692
767
|
] }),
|
|
693
768
|
celebration && /* @__PURE__ */ jsxs("section", { id: "review-options", className: "sm-review-welcome", children: [
|
|
694
769
|
/* @__PURE__ */ jsx("h2", { children: "Help us make this just right" }),
|
|
695
|
-
/* @__PURE__ */
|
|
770
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
771
|
+
"We prepared this announcement as a starting point so you don't have to write anything yourself. ",
|
|
772
|
+
can("APPROVE") ? "If everything looks right, you can simply approve it." : "Read it at your own pace, and let us know if anything needs a change."
|
|
773
|
+
] }),
|
|
696
774
|
/* @__PURE__ */ jsx("p", { children: "If you'd like, you can also correct a detail, add photographs, or tell us a little more about the couple. We'll take care of turning any additional information into the next draft." }),
|
|
697
775
|
can("ANSWER_QUESTIONS") && review.questions.length > 0 && /* @__PURE__ */ jsx(
|
|
698
776
|
"button",
|
|
@@ -720,6 +798,7 @@ function StoryReview({
|
|
|
720
798
|
className: `sm-review-actions${celebration ? " sm-review-concierge-actions" : ""}${panel !== "article" ? " sm-review-actions-editing" : ""}`,
|
|
721
799
|
"aria-label": "Review actions",
|
|
722
800
|
children: [
|
|
801
|
+
!can("APPROVE") && can("COMMENT") && (review.identity_required || review.participant) && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("feedback"), children: "Looks good to me" }),
|
|
723
802
|
can("APPROVE") && !review.published && /* @__PURE__ */ jsx(
|
|
724
803
|
"button",
|
|
725
804
|
{
|
|
@@ -753,7 +832,97 @@ function StoryReview({
|
|
|
753
832
|
] }, name)),
|
|
754
833
|
file && !pending.some(([name]) => name === "media") && /* @__PURE__ */ jsx("button", { onClick: () => setPanel("media"), children: "Continue your photo upload" })
|
|
755
834
|
] }),
|
|
756
|
-
panel
|
|
835
|
+
panel === "share" && /* @__PURE__ */ jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Share private preview", children: [
|
|
836
|
+
/* @__PURE__ */ jsx("h2", { children: "Share with your family" }),
|
|
837
|
+
errorMessage,
|
|
838
|
+
/* @__PURE__ */ jsx("p", { children: "Anyone with this link can read the private preview. To send feedback or photos, they will enter their own name and verify their email. No account is needed." }),
|
|
839
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "review-share-url", children: "Private preview link" }),
|
|
840
|
+
/* @__PURE__ */ jsx("input", { id: "review-share-url", readOnly: true, value: shareUrl, onFocus: (e) => e.currentTarget.select() }),
|
|
841
|
+
/* @__PURE__ */ jsx("button", { onClick: async () => {
|
|
842
|
+
try {
|
|
843
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
844
|
+
setNotice("Private link copied. Paste it into your message to family.");
|
|
845
|
+
} catch {
|
|
846
|
+
setNotice("Select the link above, then choose Copy on your device.");
|
|
847
|
+
}
|
|
848
|
+
}, children: "Copy private link" }),
|
|
849
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setPanel("article"), children: "Back to the story" })
|
|
850
|
+
] }),
|
|
851
|
+
panel === "identity" && /* @__PURE__ */ jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Introduce yourself", children: [
|
|
852
|
+
/* @__PURE__ */ jsx("h2", { children: identityStep === "code" ? "Check your email" : "Who is helping with the story?" }),
|
|
853
|
+
errorMessage,
|
|
854
|
+
/* @__PURE__ */ jsx("p", { children: "Your name will accompany your feedback for the newsroom. Your email stays private. No password or account is needed." }),
|
|
855
|
+
identityStep === "name" ? /* @__PURE__ */ jsxs("form", { onSubmit: async (e) => {
|
|
856
|
+
e.preventDefault();
|
|
857
|
+
setBusy(true);
|
|
858
|
+
setError("");
|
|
859
|
+
try {
|
|
860
|
+
await request("/join", { name: contributorName, email: contributorEmail });
|
|
861
|
+
await request("/verify", {});
|
|
862
|
+
setIdentityStep("code");
|
|
863
|
+
setCode("");
|
|
864
|
+
} catch (e2) {
|
|
865
|
+
setError(e2.message);
|
|
866
|
+
} finally {
|
|
867
|
+
setBusy(false);
|
|
868
|
+
}
|
|
869
|
+
}, children: [
|
|
870
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "contributor-name", children: "Your name" }),
|
|
871
|
+
/* @__PURE__ */ jsx("input", { id: "contributor-name", autoComplete: "name", value: contributorName, maxLength: 150, required: true, onChange: (e) => setContributorName(e.target.value) }),
|
|
872
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "contributor-email", children: "Your email" }),
|
|
873
|
+
/* @__PURE__ */ jsx("input", { id: "contributor-email", type: "email", autoComplete: "email", value: contributorEmail, maxLength: 320, required: true, onChange: (e) => setContributorEmail(e.target.value) }),
|
|
874
|
+
/* @__PURE__ */ jsx("p", { children: "We'll email you a code to confirm it's you." }),
|
|
875
|
+
/* @__PURE__ */ jsx("button", { disabled: busy, children: busy ? "Sending your code\u2026" : "Email me a code" })
|
|
876
|
+
] }) : /* @__PURE__ */ jsxs("form", { onSubmit: async (e) => {
|
|
877
|
+
e.preventDefault();
|
|
878
|
+
setBusy(true);
|
|
879
|
+
setError("");
|
|
880
|
+
try {
|
|
881
|
+
await request("/verify", { code });
|
|
882
|
+
await load();
|
|
883
|
+
setCode("");
|
|
884
|
+
setPanelState(requestedPanel);
|
|
885
|
+
} catch (e2) {
|
|
886
|
+
setError(e2.message);
|
|
887
|
+
} finally {
|
|
888
|
+
setBusy(false);
|
|
889
|
+
}
|
|
890
|
+
}, children: [
|
|
891
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
892
|
+
"Enter the eight-digit code sent to ",
|
|
893
|
+
/* @__PURE__ */ jsx("strong", { children: contributorEmail }),
|
|
894
|
+
". It expires in ten minutes."
|
|
895
|
+
] }),
|
|
896
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "contributor-code", children: "Email verification code" }),
|
|
897
|
+
/* @__PURE__ */ jsx("input", { id: "contributor-code", inputMode: "numeric", autoComplete: "one-time-code", pattern: "[0-9]{8}", maxLength: 8, value: code, required: true, onChange: (e) => setCode(e.target.value.replace(/\s/g, "")) }),
|
|
898
|
+
/* @__PURE__ */ jsx("button", { disabled: busy, children: busy ? "Verifying\u2026" : "Verify and continue" }),
|
|
899
|
+
/* @__PURE__ */ jsx("button", { type: "button", disabled: busy, onClick: async () => {
|
|
900
|
+
setBusy(true);
|
|
901
|
+
try {
|
|
902
|
+
await request("/verify", {});
|
|
903
|
+
setNotice("A new code was sent. Check your inbox and spam folder.");
|
|
904
|
+
} catch (e) {
|
|
905
|
+
setError(e.message);
|
|
906
|
+
} finally {
|
|
907
|
+
setBusy(false);
|
|
908
|
+
}
|
|
909
|
+
}, children: "Send a new code" }),
|
|
910
|
+
/* @__PURE__ */ jsx("button", { type: "button", disabled: busy, onClick: () => setIdentityStep("name"), children: "Use a different email" })
|
|
911
|
+
] }),
|
|
912
|
+
/* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Just read the story" })
|
|
913
|
+
] }),
|
|
914
|
+
panel === "feedback" && /* @__PURE__ */ jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Family feedback", children: [
|
|
915
|
+
/* @__PURE__ */ jsx("h2", { children: "Looks good to you?" }),
|
|
916
|
+
errorMessage,
|
|
917
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
918
|
+
"We'll send your feedback as ",
|
|
919
|
+
review.reviewer,
|
|
920
|
+
". The newsroom will request approval from the designated family reviewer separately. This will not publish the story."
|
|
921
|
+
] }),
|
|
922
|
+
/* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => void send({ action: "COMMENT", message: "This draft looks good to me.", target: { kind: "STORY" } }, "Thank you. Your feedback has been sent to the newsroom. The story is still private."), children: "Send my feedback" }),
|
|
923
|
+
/* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Back to the story" })
|
|
924
|
+
] }),
|
|
925
|
+
!["article", "identity", "share", "feedback"].includes(panel) && /* @__PURE__ */ jsxs(
|
|
757
926
|
"section",
|
|
758
927
|
{
|
|
759
928
|
ref: responseRef,
|
|
@@ -765,6 +934,7 @@ function StoryReview({
|
|
|
765
934
|
/* @__PURE__ */ jsx("h2", { children: panel === "approve" ? "Approve this exact draft" : panel === "edits" ? "Propose changes" : panel === "media" ? photoToReplace ? "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" }),
|
|
766
935
|
/* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Close" })
|
|
767
936
|
] }),
|
|
937
|
+
errorMessage,
|
|
768
938
|
panel !== "history" && /* @__PURE__ */ jsxs("div", { className: "sm-review-save-state", role: "status", "aria-live": "polite", children: [
|
|
769
939
|
currentDraft?.status === "conflict" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
770
940
|
/* @__PURE__ */ jsx("p", { children: "A response was saved in another tab or device. Your writing here is still available. Choose which version to keep." }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalemule/news-publication",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.32",
|
|
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
|
@@ -620,3 +620,10 @@
|
|
|
620
620
|
.sm-review-photo-picker button { flex: 1 1 180px; margin: 0; }
|
|
621
621
|
.sm-review-save-state button { margin: 8px 0 0; }
|
|
622
622
|
}
|
|
623
|
+
|
|
624
|
+
/* Keep family sharing alongside the preview, separate from editorial copy. */
|
|
625
|
+
.sm-review-sharing, .sm-review-identity-status { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:12px; padding:16px 0; border-bottom:1px solid var(--sm-rule, #d8d8d8); font:16px/1.5 system-ui,sans-serif; }
|
|
626
|
+
.sm-review-sharing p { flex:1 1 240px; margin:0; }
|
|
627
|
+
.sm-review-sharing button, .sm-review-identity-status button { min-height:48px; font:inherit; padding:10px 16px; cursor:pointer; }
|
|
628
|
+
.sm-review-form input#review-share-url { width:100%; font-size:16px; min-width:0; }
|
|
629
|
+
.sm-review-form label[for^="contributor-"] { display:block; margin-top:16px; }
|