@scalemule/news-publication 0.0.30 → 0.0.31
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 +176 -11
- package/dist/story-review.js +176 -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) {
|
|
@@ -542,7 +588,7 @@ function StoryReview({
|
|
|
542
588
|
/* @__PURE__ */ jsxRuntime.jsx("span", { children: review?.publication.name || publicationName })
|
|
543
589
|
] }) }),
|
|
544
590
|
/* @__PURE__ */ jsxRuntime.jsxs("main", { className: `sm-story-review${celebration ? " sm-review-celebration" : ""}`, children: [
|
|
545
|
-
error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "sm-review-alert", children: error }),
|
|
591
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("p", { ref: errorRef, tabIndex: -1, role: "alert", className: "sm-review-alert", children: error }),
|
|
546
592
|
notice && !(approved && finalEditor && notice === approvalThanks) && /* @__PURE__ */ jsxRuntime.jsx("p", { ref: noticeRef, tabIndex: -1, role: "status", className: "sm-review-notice", children: notice }),
|
|
547
593
|
verification && /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "sm-review-form", children: [
|
|
548
594
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Verify your email" }),
|
|
@@ -610,8 +656,36 @@ function StoryReview({
|
|
|
610
656
|
}
|
|
611
657
|
)
|
|
612
658
|
] }),
|
|
659
|
+
!review && error && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
660
|
+
"Ask the person who invited you to use ",
|
|
661
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Share private preview" }),
|
|
662
|
+
" or copy the complete address from their open preview. You do not need an account to read it."
|
|
663
|
+
] }),
|
|
613
664
|
!review && !error && !verification && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Opening your private preview\u2026" }),
|
|
614
665
|
review && document && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
666
|
+
canShare && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-sharing", children: [
|
|
667
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Private family preview. Share only with people you want to invite." }),
|
|
668
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => void sharePrivateLink(), children: "Share private preview" })
|
|
669
|
+
] }),
|
|
670
|
+
review.participant && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-identity-status", children: [
|
|
671
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
672
|
+
"Contributing as ",
|
|
673
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: review.reviewer })
|
|
674
|
+
] }),
|
|
675
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy || pending.length > 0 || !!file, onClick: async () => {
|
|
676
|
+
setBusy(true);
|
|
677
|
+
try {
|
|
678
|
+
await request("/leave", {});
|
|
679
|
+
setPanelState("article");
|
|
680
|
+
setIdentityStep("name");
|
|
681
|
+
await load();
|
|
682
|
+
} catch (e) {
|
|
683
|
+
setError(e.message);
|
|
684
|
+
} finally {
|
|
685
|
+
setBusy(false);
|
|
686
|
+
}
|
|
687
|
+
}, children: "Not you?" })
|
|
688
|
+
] }),
|
|
615
689
|
celebration ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-intro", children: [
|
|
616
690
|
review.revision.number > 1 ? /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
617
691
|
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Updated preview" }),
|
|
@@ -694,7 +768,10 @@ function StoryReview({
|
|
|
694
768
|
] }),
|
|
695
769
|
celebration && /* @__PURE__ */ jsxRuntime.jsxs("section", { id: "review-options", className: "sm-review-welcome", children: [
|
|
696
770
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Help us make this just right" }),
|
|
697
|
-
/* @__PURE__ */ jsxRuntime.
|
|
771
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
772
|
+
"We prepared this announcement as a starting point so you don't have to write anything yourself. ",
|
|
773
|
+
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."
|
|
774
|
+
] }),
|
|
698
775
|
/* @__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
776
|
can("ANSWER_QUESTIONS") && review.questions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
700
777
|
"button",
|
|
@@ -722,6 +799,7 @@ function StoryReview({
|
|
|
722
799
|
className: `sm-review-actions${celebration ? " sm-review-concierge-actions" : ""}${panel !== "article" ? " sm-review-actions-editing" : ""}`,
|
|
723
800
|
"aria-label": "Review actions",
|
|
724
801
|
children: [
|
|
802
|
+
!can("APPROVE") && can("COMMENT") && (review.identity_required || review.participant) && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("feedback"), children: "Looks good to me" }),
|
|
725
803
|
can("APPROVE") && !review.published && /* @__PURE__ */ jsxRuntime.jsx(
|
|
726
804
|
"button",
|
|
727
805
|
{
|
|
@@ -755,7 +833,94 @@ function StoryReview({
|
|
|
755
833
|
] }, name)),
|
|
756
834
|
file && !pending.some(([name]) => name === "media") && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("media"), children: "Continue your photo upload" })
|
|
757
835
|
] }),
|
|
758
|
-
panel
|
|
836
|
+
panel === "share" && /* @__PURE__ */ jsxRuntime.jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Share private preview", children: [
|
|
837
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Share with your family" }),
|
|
838
|
+
/* @__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." }),
|
|
839
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "review-share-url", children: "Private preview link" }),
|
|
840
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { id: "review-share-url", readOnly: true, value: shareUrl, onFocus: (e) => e.currentTarget.select() }),
|
|
841
|
+
/* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("article"), children: "Back to the story" })
|
|
850
|
+
] }),
|
|
851
|
+
panel === "identity" && /* @__PURE__ */ jsxRuntime.jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Introduce yourself", children: [
|
|
852
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: identityStep === "code" ? "Check your email" : "Who is helping with the story?" }),
|
|
853
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Your name will accompany your feedback for the newsroom. Your email stays private. No password or account is needed." }),
|
|
854
|
+
identityStep === "name" ? /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: async (e) => {
|
|
855
|
+
e.preventDefault();
|
|
856
|
+
setBusy(true);
|
|
857
|
+
setError("");
|
|
858
|
+
try {
|
|
859
|
+
await request("/join", { name: contributorName, email: contributorEmail });
|
|
860
|
+
await request("/verify", {});
|
|
861
|
+
setIdentityStep("code");
|
|
862
|
+
setCode("");
|
|
863
|
+
} catch (e2) {
|
|
864
|
+
setError(e2.message);
|
|
865
|
+
} finally {
|
|
866
|
+
setBusy(false);
|
|
867
|
+
}
|
|
868
|
+
}, children: [
|
|
869
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "contributor-name", children: "Your name" }),
|
|
870
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { id: "contributor-name", autoComplete: "name", value: contributorName, maxLength: 150, required: true, onChange: (e) => setContributorName(e.target.value) }),
|
|
871
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "contributor-email", children: "Your email" }),
|
|
872
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { id: "contributor-email", type: "email", autoComplete: "email", value: contributorEmail, maxLength: 320, required: true, onChange: (e) => setContributorEmail(e.target.value) }),
|
|
873
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "We'll email you a code to confirm it's you." }),
|
|
874
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, children: busy ? "Sending your code\u2026" : "Email me a code" })
|
|
875
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: async (e) => {
|
|
876
|
+
e.preventDefault();
|
|
877
|
+
setBusy(true);
|
|
878
|
+
setError("");
|
|
879
|
+
try {
|
|
880
|
+
await request("/verify", { code });
|
|
881
|
+
await load();
|
|
882
|
+
setCode("");
|
|
883
|
+
setPanelState(requestedPanel);
|
|
884
|
+
} catch (e2) {
|
|
885
|
+
setError(e2.message);
|
|
886
|
+
} finally {
|
|
887
|
+
setBusy(false);
|
|
888
|
+
}
|
|
889
|
+
}, children: [
|
|
890
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
891
|
+
"Enter the eight-digit code sent to ",
|
|
892
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: contributorEmail }),
|
|
893
|
+
". It expires in ten minutes."
|
|
894
|
+
] }),
|
|
895
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "contributor-code", children: "Email verification code" }),
|
|
896
|
+
/* @__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, "")) }),
|
|
897
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, children: busy ? "Verifying\u2026" : "Verify and continue" }),
|
|
898
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", disabled: busy, onClick: async () => {
|
|
899
|
+
setBusy(true);
|
|
900
|
+
try {
|
|
901
|
+
await request("/verify", {});
|
|
902
|
+
setNotice("A new code was sent. Check your inbox and spam folder.");
|
|
903
|
+
} catch (e) {
|
|
904
|
+
setError(e.message);
|
|
905
|
+
} finally {
|
|
906
|
+
setBusy(false);
|
|
907
|
+
}
|
|
908
|
+
}, children: "Send a new code" }),
|
|
909
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", disabled: busy, onClick: () => setIdentityStep("name"), children: "Use a different email" })
|
|
910
|
+
] }),
|
|
911
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Just read the story" })
|
|
912
|
+
] }),
|
|
913
|
+
panel === "feedback" && /* @__PURE__ */ jsxRuntime.jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Family feedback", children: [
|
|
914
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Looks good to you?" }),
|
|
915
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
916
|
+
"We'll send your feedback as ",
|
|
917
|
+
review.reviewer,
|
|
918
|
+
". The newsroom will request approval from the designated family reviewer separately. This will not publish the story."
|
|
919
|
+
] }),
|
|
920
|
+
/* @__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" }),
|
|
921
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Back to the story" })
|
|
922
|
+
] }),
|
|
923
|
+
!["article", "identity", "share", "feedback"].includes(panel) && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
759
924
|
"section",
|
|
760
925
|
{
|
|
761
926
|
ref: responseRef,
|
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) {
|
|
@@ -540,7 +586,7 @@ function StoryReview({
|
|
|
540
586
|
/* @__PURE__ */ jsx("span", { children: review?.publication.name || publicationName })
|
|
541
587
|
] }) }),
|
|
542
588
|
/* @__PURE__ */ jsxs("main", { className: `sm-story-review${celebration ? " sm-review-celebration" : ""}`, children: [
|
|
543
|
-
error && /* @__PURE__ */ jsx("p", { role: "alert", className: "sm-review-alert", children: error }),
|
|
589
|
+
error && /* @__PURE__ */ jsx("p", { ref: errorRef, tabIndex: -1, role: "alert", className: "sm-review-alert", children: error }),
|
|
544
590
|
notice && !(approved && finalEditor && notice === approvalThanks) && /* @__PURE__ */ jsx("p", { ref: noticeRef, tabIndex: -1, role: "status", className: "sm-review-notice", children: notice }),
|
|
545
591
|
verification && /* @__PURE__ */ jsxs("section", { className: "sm-review-form", children: [
|
|
546
592
|
/* @__PURE__ */ jsx("h2", { children: "Verify your email" }),
|
|
@@ -608,8 +654,36 @@ function StoryReview({
|
|
|
608
654
|
}
|
|
609
655
|
)
|
|
610
656
|
] }),
|
|
657
|
+
!review && error && /* @__PURE__ */ jsxs("p", { children: [
|
|
658
|
+
"Ask the person who invited you to use ",
|
|
659
|
+
/* @__PURE__ */ jsx("strong", { children: "Share private preview" }),
|
|
660
|
+
" or copy the complete address from their open preview. You do not need an account to read it."
|
|
661
|
+
] }),
|
|
611
662
|
!review && !error && !verification && /* @__PURE__ */ jsx("p", { role: "status", children: "Opening your private preview\u2026" }),
|
|
612
663
|
review && document && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
664
|
+
canShare && /* @__PURE__ */ jsxs("div", { className: "sm-review-sharing", children: [
|
|
665
|
+
/* @__PURE__ */ jsx("p", { children: "Private family preview. Share only with people you want to invite." }),
|
|
666
|
+
/* @__PURE__ */ jsx("button", { onClick: () => void sharePrivateLink(), children: "Share private preview" })
|
|
667
|
+
] }),
|
|
668
|
+
review.participant && /* @__PURE__ */ jsxs("div", { className: "sm-review-identity-status", children: [
|
|
669
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
670
|
+
"Contributing as ",
|
|
671
|
+
/* @__PURE__ */ jsx("strong", { children: review.reviewer })
|
|
672
|
+
] }),
|
|
673
|
+
/* @__PURE__ */ jsx("button", { disabled: busy || pending.length > 0 || !!file, onClick: async () => {
|
|
674
|
+
setBusy(true);
|
|
675
|
+
try {
|
|
676
|
+
await request("/leave", {});
|
|
677
|
+
setPanelState("article");
|
|
678
|
+
setIdentityStep("name");
|
|
679
|
+
await load();
|
|
680
|
+
} catch (e) {
|
|
681
|
+
setError(e.message);
|
|
682
|
+
} finally {
|
|
683
|
+
setBusy(false);
|
|
684
|
+
}
|
|
685
|
+
}, children: "Not you?" })
|
|
686
|
+
] }),
|
|
613
687
|
celebration ? /* @__PURE__ */ jsxs("div", { className: "sm-review-intro", children: [
|
|
614
688
|
review.revision.number > 1 ? /* @__PURE__ */ jsxs("p", { children: [
|
|
615
689
|
/* @__PURE__ */ jsx("strong", { children: "Updated preview" }),
|
|
@@ -692,7 +766,10 @@ function StoryReview({
|
|
|
692
766
|
] }),
|
|
693
767
|
celebration && /* @__PURE__ */ jsxs("section", { id: "review-options", className: "sm-review-welcome", children: [
|
|
694
768
|
/* @__PURE__ */ jsx("h2", { children: "Help us make this just right" }),
|
|
695
|
-
/* @__PURE__ */
|
|
769
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
770
|
+
"We prepared this announcement as a starting point so you don't have to write anything yourself. ",
|
|
771
|
+
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."
|
|
772
|
+
] }),
|
|
696
773
|
/* @__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
774
|
can("ANSWER_QUESTIONS") && review.questions.length > 0 && /* @__PURE__ */ jsx(
|
|
698
775
|
"button",
|
|
@@ -720,6 +797,7 @@ function StoryReview({
|
|
|
720
797
|
className: `sm-review-actions${celebration ? " sm-review-concierge-actions" : ""}${panel !== "article" ? " sm-review-actions-editing" : ""}`,
|
|
721
798
|
"aria-label": "Review actions",
|
|
722
799
|
children: [
|
|
800
|
+
!can("APPROVE") && can("COMMENT") && (review.identity_required || review.participant) && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("feedback"), children: "Looks good to me" }),
|
|
723
801
|
can("APPROVE") && !review.published && /* @__PURE__ */ jsx(
|
|
724
802
|
"button",
|
|
725
803
|
{
|
|
@@ -753,7 +831,94 @@ function StoryReview({
|
|
|
753
831
|
] }, name)),
|
|
754
832
|
file && !pending.some(([name]) => name === "media") && /* @__PURE__ */ jsx("button", { onClick: () => setPanel("media"), children: "Continue your photo upload" })
|
|
755
833
|
] }),
|
|
756
|
-
panel
|
|
834
|
+
panel === "share" && /* @__PURE__ */ jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Share private preview", children: [
|
|
835
|
+
/* @__PURE__ */ jsx("h2", { children: "Share with your family" }),
|
|
836
|
+
/* @__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." }),
|
|
837
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "review-share-url", children: "Private preview link" }),
|
|
838
|
+
/* @__PURE__ */ jsx("input", { id: "review-share-url", readOnly: true, value: shareUrl, onFocus: (e) => e.currentTarget.select() }),
|
|
839
|
+
/* @__PURE__ */ jsx("button", { onClick: async () => {
|
|
840
|
+
try {
|
|
841
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
842
|
+
setNotice("Private link copied. Paste it into your message to family.");
|
|
843
|
+
} catch {
|
|
844
|
+
setNotice("Select the link above, then choose Copy on your device.");
|
|
845
|
+
}
|
|
846
|
+
}, children: "Copy private link" }),
|
|
847
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setPanel("article"), children: "Back to the story" })
|
|
848
|
+
] }),
|
|
849
|
+
panel === "identity" && /* @__PURE__ */ jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Introduce yourself", children: [
|
|
850
|
+
/* @__PURE__ */ jsx("h2", { children: identityStep === "code" ? "Check your email" : "Who is helping with the story?" }),
|
|
851
|
+
/* @__PURE__ */ jsx("p", { children: "Your name will accompany your feedback for the newsroom. Your email stays private. No password or account is needed." }),
|
|
852
|
+
identityStep === "name" ? /* @__PURE__ */ jsxs("form", { onSubmit: async (e) => {
|
|
853
|
+
e.preventDefault();
|
|
854
|
+
setBusy(true);
|
|
855
|
+
setError("");
|
|
856
|
+
try {
|
|
857
|
+
await request("/join", { name: contributorName, email: contributorEmail });
|
|
858
|
+
await request("/verify", {});
|
|
859
|
+
setIdentityStep("code");
|
|
860
|
+
setCode("");
|
|
861
|
+
} catch (e2) {
|
|
862
|
+
setError(e2.message);
|
|
863
|
+
} finally {
|
|
864
|
+
setBusy(false);
|
|
865
|
+
}
|
|
866
|
+
}, children: [
|
|
867
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "contributor-name", children: "Your name" }),
|
|
868
|
+
/* @__PURE__ */ jsx("input", { id: "contributor-name", autoComplete: "name", value: contributorName, maxLength: 150, required: true, onChange: (e) => setContributorName(e.target.value) }),
|
|
869
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "contributor-email", children: "Your email" }),
|
|
870
|
+
/* @__PURE__ */ jsx("input", { id: "contributor-email", type: "email", autoComplete: "email", value: contributorEmail, maxLength: 320, required: true, onChange: (e) => setContributorEmail(e.target.value) }),
|
|
871
|
+
/* @__PURE__ */ jsx("p", { children: "We'll email you a code to confirm it's you." }),
|
|
872
|
+
/* @__PURE__ */ jsx("button", { disabled: busy, children: busy ? "Sending your code\u2026" : "Email me a code" })
|
|
873
|
+
] }) : /* @__PURE__ */ jsxs("form", { onSubmit: async (e) => {
|
|
874
|
+
e.preventDefault();
|
|
875
|
+
setBusy(true);
|
|
876
|
+
setError("");
|
|
877
|
+
try {
|
|
878
|
+
await request("/verify", { code });
|
|
879
|
+
await load();
|
|
880
|
+
setCode("");
|
|
881
|
+
setPanelState(requestedPanel);
|
|
882
|
+
} catch (e2) {
|
|
883
|
+
setError(e2.message);
|
|
884
|
+
} finally {
|
|
885
|
+
setBusy(false);
|
|
886
|
+
}
|
|
887
|
+
}, children: [
|
|
888
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
889
|
+
"Enter the eight-digit code sent to ",
|
|
890
|
+
/* @__PURE__ */ jsx("strong", { children: contributorEmail }),
|
|
891
|
+
". It expires in ten minutes."
|
|
892
|
+
] }),
|
|
893
|
+
/* @__PURE__ */ jsx("label", { htmlFor: "contributor-code", children: "Email verification code" }),
|
|
894
|
+
/* @__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, "")) }),
|
|
895
|
+
/* @__PURE__ */ jsx("button", { disabled: busy, children: busy ? "Verifying\u2026" : "Verify and continue" }),
|
|
896
|
+
/* @__PURE__ */ jsx("button", { type: "button", disabled: busy, onClick: async () => {
|
|
897
|
+
setBusy(true);
|
|
898
|
+
try {
|
|
899
|
+
await request("/verify", {});
|
|
900
|
+
setNotice("A new code was sent. Check your inbox and spam folder.");
|
|
901
|
+
} catch (e) {
|
|
902
|
+
setError(e.message);
|
|
903
|
+
} finally {
|
|
904
|
+
setBusy(false);
|
|
905
|
+
}
|
|
906
|
+
}, children: "Send a new code" }),
|
|
907
|
+
/* @__PURE__ */ jsx("button", { type: "button", disabled: busy, onClick: () => setIdentityStep("name"), children: "Use a different email" })
|
|
908
|
+
] }),
|
|
909
|
+
/* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Just read the story" })
|
|
910
|
+
] }),
|
|
911
|
+
panel === "feedback" && /* @__PURE__ */ jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Family feedback", children: [
|
|
912
|
+
/* @__PURE__ */ jsx("h2", { children: "Looks good to you?" }),
|
|
913
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
914
|
+
"We'll send your feedback as ",
|
|
915
|
+
review.reviewer,
|
|
916
|
+
". The newsroom will request approval from the designated family reviewer separately. This will not publish the story."
|
|
917
|
+
] }),
|
|
918
|
+
/* @__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" }),
|
|
919
|
+
/* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Back to the story" })
|
|
920
|
+
] }),
|
|
921
|
+
!["article", "identity", "share", "feedback"].includes(panel) && /* @__PURE__ */ jsxs(
|
|
757
922
|
"section",
|
|
758
923
|
{
|
|
759
924
|
ref: responseRef,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalemule/news-publication",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.31",
|
|
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; }
|