@scalemule/news-publication 0.0.33 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/story-review.cjs +164 -15
- package/dist/story-review.d.cts +5 -1
- package/dist/story-review.d.ts +5 -1
- package/dist/story-review.js +164 -15
- package/package.json +1 -1
- package/story-review.css +5 -0
package/README.md
CHANGED
|
@@ -111,3 +111,20 @@ The review endpoint returns the decision for the exact invited revision. Editors
|
|
|
111
111
|
### Publication framing for private previews (0.0.28)
|
|
112
112
|
|
|
113
113
|
Pass `publicationHeader` and `publicationFooter` React nodes to `StoryReview` to reuse a publication's real navigation, masthead, and footer. The private banner remains above the publication header and the review controls remain separate from the story. The standalone masthead remains the default. The host must suppress analytics on private review routes; public article indexing and sharing features are not mounted by these slots. This patch changes presentation only; review permissions and publication gates are unchanged.
|
|
114
|
+
|
|
115
|
+
### Private review sign-in and email options
|
|
116
|
+
|
|
117
|
+
`StoryReview` accepts `readerAccountUrl` and a client `renderSignIn(returnTo)`
|
|
118
|
+
function so the host publication can reuse its existing account and sign-in
|
|
119
|
+
components. Hosts must implement the `/api/news/review/:session/account`
|
|
120
|
+
server adapter to connect the authenticated, verified reader through Blog’s
|
|
121
|
+
`join-account` endpoint. Never trust a browser-supplied email or verification flag.
|
|
122
|
+
The private return path belongs in the host’s signed, HttpOnly OAuth flow cookie,
|
|
123
|
+
not the provider’s authorization URL. Contributor access does not imply subject
|
|
124
|
+
approval authority.
|
|
125
|
+
|
|
126
|
+
Optional questions can be browsed and copied before identity verification.
|
|
127
|
+
`feedbackEmail` enables mail-app links containing the story title, exact revision
|
|
128
|
+
reference and optional answers, never the invitation credential. Configure a
|
|
129
|
+
monitored mailbox; received email must be recorded by the newsroom. Online
|
|
130
|
+
answers retain their existing autosave and explicit-send behavior.
|
package/dist/story-review.cjs
CHANGED
|
@@ -158,7 +158,18 @@ function useReviewDrafts(session, revision, initial, request) {
|
|
|
158
158
|
}
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
|
+
async function encodePendingPhoto(value) {
|
|
162
|
+
const { file, ...details } = value;
|
|
163
|
+
return { ...details, file: { bytes: await file.arrayBuffer(), name: file.name, type: file.type, lastModified: file.lastModified } };
|
|
164
|
+
}
|
|
165
|
+
function decodePendingPhoto(value) {
|
|
166
|
+
if (!value) return void 0;
|
|
167
|
+
if (value.file instanceof File) return value;
|
|
168
|
+
const file = value.file;
|
|
169
|
+
return { ...value, file: new File([file.bytes], file.name, { type: file.type, lastModified: file.lastModified }) };
|
|
170
|
+
}
|
|
161
171
|
async function stored(key, value) {
|
|
172
|
+
const encoded = value ? await encodePendingPhoto(value) : value;
|
|
162
173
|
const db = await new Promise((resolve, reject) => {
|
|
163
174
|
const request = indexedDB.open("story-review-pending-media", 1);
|
|
164
175
|
request.onupgradeneeded = () => request.result.createObjectStore("photos");
|
|
@@ -169,8 +180,14 @@ async function stored(key, value) {
|
|
|
169
180
|
return await new Promise((resolve, reject) => {
|
|
170
181
|
const tx = db.transaction("photos", value === void 0 ? "readonly" : "readwrite");
|
|
171
182
|
const store = tx.objectStore("photos");
|
|
172
|
-
const request = value === void 0 ? store.get(key) : value === null ? store.delete(key) : store.put(
|
|
173
|
-
tx.oncomplete = () =>
|
|
183
|
+
const request = value === void 0 ? store.get(key) : value === null ? store.delete(key) : store.put(encoded, key);
|
|
184
|
+
tx.oncomplete = () => {
|
|
185
|
+
try {
|
|
186
|
+
resolve(value === void 0 ? decodePendingPhoto(request.result) : void 0);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
reject(error);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
174
191
|
tx.onerror = () => reject(tx.error);
|
|
175
192
|
tx.onabort = () => reject(tx.error);
|
|
176
193
|
});
|
|
@@ -274,6 +291,23 @@ function proseDiff(before, after) {
|
|
|
274
291
|
emit("same", a.slice(endA).join(""));
|
|
275
292
|
return out;
|
|
276
293
|
}
|
|
294
|
+
|
|
295
|
+
// src/story-review-email.ts
|
|
296
|
+
function reviewEmailText(review, answers) {
|
|
297
|
+
const header = `Private story feedback \u2014 not published
|
|
298
|
+
${review.revision.document.title}
|
|
299
|
+
Preview v${review.revision.number} \xB7 Reference ${review.revision.id}
|
|
300
|
+
|
|
301
|
+
Your name:
|
|
302
|
+
|
|
303
|
+
`;
|
|
304
|
+
return header + (answers === void 0 ? "My feedback:\n" : "These questions are optional. Answer only what you would like to share.\n\n" + review.questions.map((q, i) => `${i + 1}. ${q.prompt}
|
|
305
|
+
${answers[q.id] || ""}`).join("\n\n"));
|
|
306
|
+
}
|
|
307
|
+
function reviewEmailLink(email, title, text) {
|
|
308
|
+
if (!email || !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email)) return void 0;
|
|
309
|
+
return `mailto:${encodeURIComponent(email)}?subject=${encodeURIComponent(`Private story feedback: ${title}`)}&body=${encodeURIComponent(text)}`;
|
|
310
|
+
}
|
|
277
311
|
function EditorialNotices({ editorial }) {
|
|
278
312
|
return editorial ? /* @__PURE__ */ jsxRuntime.jsxs("aside", { "aria-label": "Editorial notices", children: [
|
|
279
313
|
editorial.content_kind && editorial.content_kind !== "EDITORIAL" && /* @__PURE__ */ jsxRuntime.jsx("p", { children: human(editorial.content_kind) }),
|
|
@@ -293,15 +327,40 @@ function StoryReview({
|
|
|
293
327
|
publicationName,
|
|
294
328
|
nameplateUrl,
|
|
295
329
|
publicationHeader,
|
|
296
|
-
publicationFooter
|
|
330
|
+
publicationFooter,
|
|
331
|
+
readerAccountUrl,
|
|
332
|
+
renderSignIn,
|
|
333
|
+
feedbackEmail
|
|
297
334
|
}) {
|
|
298
335
|
const base = `/api/news/review/${encodeURIComponent(sessionId)}`;
|
|
299
336
|
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
337
|
const [shareUrl, setShareUrl] = react.useState(""), [canShare, setCanShare] = react.useState(false);
|
|
301
338
|
const [contributorName, setContributorName] = react.useState(""), [contributorEmail, setContributorEmail] = react.useState("");
|
|
302
339
|
const [identityStep, setIdentityStep] = react.useState("name"), [requestedPanel, setRequestedPanel] = react.useState("comment");
|
|
340
|
+
const [reader, setReader] = react.useState(null);
|
|
341
|
+
const [readerLoading, setReaderLoading] = react.useState(!!readerAccountUrl);
|
|
342
|
+
const [useEmailCode, setUseEmailCode] = react.useState(!renderSignIn);
|
|
343
|
+
const [returnPanel, setReturnPanel] = react.useState("");
|
|
344
|
+
const resumed = react.useRef(false);
|
|
345
|
+
const [copiedQuestions, setCopiedQuestions] = react.useState(false);
|
|
346
|
+
react.useEffect(() => {
|
|
347
|
+
if (!readerAccountUrl) return;
|
|
348
|
+
let active = true;
|
|
349
|
+
void fetch(readerAccountUrl, { credentials: "same-origin", cache: "no-store", signal: AbortSignal.timeout(15e3) }).then(async (response) => {
|
|
350
|
+
if (response.ok) {
|
|
351
|
+
const data = await response.json();
|
|
352
|
+
if (active) setReader(data.user || null);
|
|
353
|
+
}
|
|
354
|
+
}).catch(() => {
|
|
355
|
+
}).finally(() => {
|
|
356
|
+
if (active) setReaderLoading(false);
|
|
357
|
+
});
|
|
358
|
+
return () => {
|
|
359
|
+
active = false;
|
|
360
|
+
};
|
|
361
|
+
}, [readerAccountUrl]);
|
|
303
362
|
function setPanel(next) {
|
|
304
|
-
if (review?.identity_required && !["article", "history", "identity", "share"].includes(next)) {
|
|
363
|
+
if (review?.identity_required && !["article", "history", "identity", "share", "question-list"].includes(next)) {
|
|
305
364
|
setRequestedPanel(next);
|
|
306
365
|
setPanelState("identity");
|
|
307
366
|
} else setPanelState(next);
|
|
@@ -399,10 +458,41 @@ function StoryReview({
|
|
|
399
458
|
setReview(data);
|
|
400
459
|
}
|
|
401
460
|
}
|
|
461
|
+
async function connectReader(target = requestedPanel) {
|
|
462
|
+
setBusy(true);
|
|
463
|
+
setError("");
|
|
464
|
+
try {
|
|
465
|
+
await request("/account", {});
|
|
466
|
+
await load();
|
|
467
|
+
setPanelState(target);
|
|
468
|
+
setReturnPanel("");
|
|
469
|
+
const url = new URL(window.location.href);
|
|
470
|
+
url.searchParams.delete("respond");
|
|
471
|
+
window.history.replaceState(null, "", url.pathname + url.search);
|
|
472
|
+
} catch (e) {
|
|
473
|
+
setError(e.message);
|
|
474
|
+
setPanelState("identity");
|
|
475
|
+
} finally {
|
|
476
|
+
setBusy(false);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
react.useEffect(() => {
|
|
480
|
+
if (!returnPanel || !review || !reader?.email_verified || resumed.current) return;
|
|
481
|
+
resumed.current = true;
|
|
482
|
+
void connectReader(returnPanel);
|
|
483
|
+
}, [returnPanel, review, reader]);
|
|
402
484
|
react.useEffect(() => {
|
|
403
485
|
let active = true;
|
|
404
486
|
void (async () => {
|
|
405
487
|
try {
|
|
488
|
+
const returnUrl = new URL(window.location.href);
|
|
489
|
+
if (returnUrl.searchParams.get("signin") === "retry") setError("Sign-in wasn\u2019t completed. Try again or use an email code.");
|
|
490
|
+
const respond = returnUrl.searchParams.get("respond") || "";
|
|
491
|
+
if (["feedback", "changes", "questions", "media", "comment", "edits"].includes(respond)) {
|
|
492
|
+
setRequestedPanel(respond);
|
|
493
|
+
setReturnPanel(respond);
|
|
494
|
+
setPanelState("identity");
|
|
495
|
+
}
|
|
406
496
|
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
497
|
let exchange = {};
|
|
408
498
|
try {
|
|
@@ -414,7 +504,9 @@ function StoryReview({
|
|
|
414
504
|
if (address) {
|
|
415
505
|
const canonical = new URL(address, window.location.origin);
|
|
416
506
|
if (canonical.origin === window.location.origin) {
|
|
417
|
-
|
|
507
|
+
const address2 = new URL(canonical);
|
|
508
|
+
if (respond) address2.searchParams.set("respond", respond);
|
|
509
|
+
window.history.replaceState(null, "", address2.pathname + address2.search);
|
|
418
510
|
if (active) {
|
|
419
511
|
setShareUrl(canonical.href);
|
|
420
512
|
setCanShare(exchange.can_share === true);
|
|
@@ -579,6 +671,15 @@ function StoryReview({
|
|
|
579
671
|
(r) => r.id === (compareTo || review?.revision.id)
|
|
580
672
|
);
|
|
581
673
|
const errorMessage = error ? /* @__PURE__ */ jsxRuntime.jsx("p", { ref: errorRef, tabIndex: -1, role: "alert", className: "sm-review-alert", children: error }) : null;
|
|
674
|
+
const emailLink = review ? reviewEmailLink(feedbackEmail, review.revision.document.title, reviewEmailText(review)) : void 0;
|
|
675
|
+
const questionText = review ? reviewEmailText(review, answers) : "";
|
|
676
|
+
const questionsEmail = review ? reviewEmailLink(feedbackEmail, review.revision.document.title, questionText) : void 0;
|
|
677
|
+
const signInReturn = shareUrl ? new URL(shareUrl).pathname + new URL(shareUrl).search + `&respond=${encodeURIComponent(requestedPanel)}` : "";
|
|
678
|
+
const emailAlternative = emailLink && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "sm-review-email-alternative", children: [
|
|
679
|
+
"Prefer email? ",
|
|
680
|
+
/* @__PURE__ */ jsxRuntime.jsx("a", { href: emailLink, children: "Email the newsroom" }),
|
|
681
|
+
". Your email app will open; send your message there."
|
|
682
|
+
] });
|
|
582
683
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `sm-review-frame${publicationHeader ? " sm-review-publication-frame" : ""}`, children: [
|
|
583
684
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "sm-review-banner-wrap", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-private", children: [
|
|
584
685
|
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: review?.published ? "Private review" : "Private preview \xB7 Not published" }),
|
|
@@ -779,7 +880,7 @@ function StoryReview({
|
|
|
779
880
|
{
|
|
780
881
|
onClick: () => {
|
|
781
882
|
setMoreQuestions(false);
|
|
782
|
-
setPanel("questions");
|
|
883
|
+
setPanel(review.identity_required ? "question-list" : "questions");
|
|
783
884
|
},
|
|
784
885
|
children: "Tell Us More"
|
|
785
886
|
}
|
|
@@ -792,7 +893,8 @@ function StoryReview({
|
|
|
792
893
|
children: "Prefer Not to Publish"
|
|
793
894
|
}
|
|
794
895
|
),
|
|
795
|
-
can("COMMENT") && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "sm-review-quiet", onClick: () => setPanel("comment"), children: "Leave a Comment" })
|
|
896
|
+
can("COMMENT") && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "sm-review-quiet", onClick: () => setPanel("comment"), children: "Leave a Comment" }),
|
|
897
|
+
emailAlternative
|
|
796
898
|
] }),
|
|
797
899
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
798
900
|
"nav",
|
|
@@ -815,7 +917,7 @@ function StoryReview({
|
|
|
815
917
|
can("UPLOAD_MEDIA") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => openMedia(), children: celebration ? "Add Photos" : "Add photos" }),
|
|
816
918
|
!celebration && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
817
919
|
can("PROPOSE_EDITS") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("edits"), children: "Suggest edits" }),
|
|
818
|
-
can("ANSWER_QUESTIONS") && review.questions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("questions"), children: "Answer questions" }),
|
|
920
|
+
can("ANSWER_QUESTIONS") && review.questions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel(review.identity_required ? "question-list" : "questions"), children: "Answer questions" }),
|
|
819
921
|
can("COMMENT") && /* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("comment"), children: "Leave a comment" })
|
|
820
922
|
] })
|
|
821
923
|
]
|
|
@@ -837,7 +939,10 @@ function StoryReview({
|
|
|
837
939
|
panel === "share" && /* @__PURE__ */ jsxRuntime.jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Share private preview", children: [
|
|
838
940
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Share with your family" }),
|
|
839
941
|
errorMessage,
|
|
840
|
-
/* @__PURE__ */ jsxRuntime.
|
|
942
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
943
|
+
"Anyone with this link can read the private preview. ",
|
|
944
|
+
renderSignIn ? "To contribute, they can continue with Google or use an email code." : "To send feedback or photos, they will enter their own name and verify their email. No account is needed."
|
|
945
|
+
] }),
|
|
841
946
|
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "review-share-url", children: "Private preview link" }),
|
|
842
947
|
/* @__PURE__ */ jsxRuntime.jsx("input", { id: "review-share-url", readOnly: true, value: shareUrl, onFocus: (e) => e.currentTarget.select() }),
|
|
843
948
|
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: async () => {
|
|
@@ -850,11 +955,48 @@ function StoryReview({
|
|
|
850
955
|
}, children: "Copy private link" }),
|
|
851
956
|
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("article"), children: "Back to the story" })
|
|
852
957
|
] }),
|
|
958
|
+
panel === "question-list" && /* @__PURE__ */ jsxRuntime.jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Optional story questions", children: [
|
|
959
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm-review-form-heading", children: [
|
|
960
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "Tell us a little more" }),
|
|
961
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setPanel("article"), children: "Close" })
|
|
962
|
+
] }),
|
|
963
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Completely optional. Read the questions first, then answer online or copy them into an email. Even one answer helps." }),
|
|
964
|
+
/* @__PURE__ */ jsxRuntime.jsx("ol", { className: "sm-review-question-list", children: review.questions.filter((q, i) => moreQuestions || i < 5 || q.required).map((q) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: q.prompt }, q.id)) }),
|
|
965
|
+
!moreQuestions && review.questions.length > 5 && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setMoreQuestions(true), children: "See all questions" }),
|
|
966
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "sm-review-primary", onClick: () => setPanel("questions"), children: "Answer online" }),
|
|
967
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Sign in to save your answers as you go. You can return whenever it suits you." }),
|
|
968
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: async () => {
|
|
969
|
+
try {
|
|
970
|
+
await navigator.clipboard.writeText(questionText);
|
|
971
|
+
setNotice("Questions copied. Paste them into an email and answer as many as you like.");
|
|
972
|
+
} catch {
|
|
973
|
+
setCopiedQuestions(true);
|
|
974
|
+
}
|
|
975
|
+
}, children: "Copy all questions" }),
|
|
976
|
+
copiedQuestions && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
977
|
+
"Copy these questions",
|
|
978
|
+
/* @__PURE__ */ jsxRuntime.jsx("textarea", { readOnly: true, rows: 10, value: questionText, onFocus: (e) => e.currentTarget.select() })
|
|
979
|
+
] }),
|
|
980
|
+
questionsEmail && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
981
|
+
/* @__PURE__ */ jsxRuntime.jsx("a", { href: questionsEmail, children: "Answer by email" }),
|
|
982
|
+
" \u2014 opens your email app with the questions ready."
|
|
983
|
+
] })
|
|
984
|
+
] }),
|
|
853
985
|
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" : "
|
|
986
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: identityStep === "code" ? "Check your email" : "Continue to help with the story" }),
|
|
855
987
|
errorMessage,
|
|
856
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Your name
|
|
857
|
-
identityStep === "name"
|
|
988
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Your name accompanies your feedback. Your email stays private." }),
|
|
989
|
+
identityStep === "name" && renderSignIn && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
990
|
+
readerLoading ? /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Checking your sign-in\u2026" }) : reader?.email_verified ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
991
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { className: "sm-review-primary", disabled: busy, onClick: () => void connectReader(), children: [
|
|
992
|
+
"Continue as ",
|
|
993
|
+
reader.full_name || reader.email
|
|
994
|
+
] }),
|
|
995
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "Your reader account is ready. No email code is needed." })
|
|
996
|
+
] }) : renderSignIn(signInReturn),
|
|
997
|
+
!useEmailCode && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "sm-review-quiet", onClick: () => setUseEmailCode(true), children: "Use an email code instead" })
|
|
998
|
+
] }),
|
|
999
|
+
identityStep === "name" && useEmailCode && /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: async (e) => {
|
|
858
1000
|
e.preventDefault();
|
|
859
1001
|
setBusy(true);
|
|
860
1002
|
setError("");
|
|
@@ -874,8 +1016,10 @@ function StoryReview({
|
|
|
874
1016
|
/* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "contributor-email", children: "Your email" }),
|
|
875
1017
|
/* @__PURE__ */ jsxRuntime.jsx("input", { id: "contributor-email", type: "email", autoComplete: "email", value: contributorEmail, maxLength: 320, required: true, onChange: (e) => setContributorEmail(e.target.value) }),
|
|
876
1018
|
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "We'll email you a code to confirm it's you." }),
|
|
1019
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: "No password or account is needed for this option." }),
|
|
877
1020
|
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, children: busy ? "Sending your code\u2026" : "Email me a code" })
|
|
878
|
-
] })
|
|
1021
|
+
] }),
|
|
1022
|
+
identityStep === "code" && /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: async (e) => {
|
|
879
1023
|
e.preventDefault();
|
|
880
1024
|
setBusy(true);
|
|
881
1025
|
setError("");
|
|
@@ -911,6 +1055,7 @@ function StoryReview({
|
|
|
911
1055
|
}, children: "Send a new code" }),
|
|
912
1056
|
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", disabled: busy, onClick: () => setIdentityStep("name"), children: "Use a different email" })
|
|
913
1057
|
] }),
|
|
1058
|
+
emailAlternative,
|
|
914
1059
|
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Just read the story" })
|
|
915
1060
|
] }),
|
|
916
1061
|
panel === "feedback" && /* @__PURE__ */ jsxRuntime.jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Family feedback", children: [
|
|
@@ -924,7 +1069,7 @@ function StoryReview({
|
|
|
924
1069
|
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "sm-review-primary", 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.", false), children: "Send my feedback" }),
|
|
925
1070
|
/* @__PURE__ */ jsxRuntime.jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Back to the story" })
|
|
926
1071
|
] }),
|
|
927
|
-
!["article", "identity", "share", "feedback"].includes(panel) && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1072
|
+
!["article", "identity", "share", "feedback", "question-list"].includes(panel) && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
928
1073
|
"section",
|
|
929
1074
|
{
|
|
930
1075
|
ref: responseRef,
|
|
@@ -1045,7 +1190,11 @@ function StoryReview({
|
|
|
1045
1190
|
onClick: () => setMoreQuestions(true),
|
|
1046
1191
|
children: "Answer More Questions"
|
|
1047
1192
|
}
|
|
1048
|
-
)
|
|
1193
|
+
),
|
|
1194
|
+
questionsEmail && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
1195
|
+
/* @__PURE__ */ jsxRuntime.jsx("a", { href: questionsEmail, children: "Email these answers instead" }),
|
|
1196
|
+
". Send them from your email app; your online draft stays saved."
|
|
1197
|
+
] })
|
|
1049
1198
|
]
|
|
1050
1199
|
}
|
|
1051
1200
|
),
|
package/dist/story-review.d.cts
CHANGED
|
@@ -32,13 +32,17 @@ type Revision = {
|
|
|
32
32
|
document: Document;
|
|
33
33
|
content_html: string;
|
|
34
34
|
};
|
|
35
|
-
declare function StoryReview({ sessionId, publicationName, nameplateUrl, publicationHeader, publicationFooter, }: {
|
|
35
|
+
declare function StoryReview({ sessionId, publicationName, nameplateUrl, publicationHeader, publicationFooter, readerAccountUrl, renderSignIn, feedbackEmail, }: {
|
|
36
36
|
sessionId: string;
|
|
37
37
|
publicationName: string;
|
|
38
38
|
nameplateUrl?: string;
|
|
39
39
|
/** The publication supplies its real shell; private content still loads only through the review capability. */
|
|
40
40
|
publicationHeader?: react__default.ReactNode;
|
|
41
41
|
publicationFooter?: react__default.ReactNode;
|
|
42
|
+
/** Reuse the publication's existing account and sign-in components. */
|
|
43
|
+
readerAccountUrl?: string;
|
|
44
|
+
renderSignIn?: (returnTo: string) => react__default.ReactNode;
|
|
45
|
+
feedbackEmail?: string;
|
|
42
46
|
}): react__default.JSX.Element;
|
|
43
47
|
/** Paragraph comparisons preserve readable prose and identify title/dek and media changes. */
|
|
44
48
|
declare function ReviewDiff({ before, after, }: {
|
package/dist/story-review.d.ts
CHANGED
|
@@ -32,13 +32,17 @@ type Revision = {
|
|
|
32
32
|
document: Document;
|
|
33
33
|
content_html: string;
|
|
34
34
|
};
|
|
35
|
-
declare function StoryReview({ sessionId, publicationName, nameplateUrl, publicationHeader, publicationFooter, }: {
|
|
35
|
+
declare function StoryReview({ sessionId, publicationName, nameplateUrl, publicationHeader, publicationFooter, readerAccountUrl, renderSignIn, feedbackEmail, }: {
|
|
36
36
|
sessionId: string;
|
|
37
37
|
publicationName: string;
|
|
38
38
|
nameplateUrl?: string;
|
|
39
39
|
/** The publication supplies its real shell; private content still loads only through the review capability. */
|
|
40
40
|
publicationHeader?: react__default.ReactNode;
|
|
41
41
|
publicationFooter?: react__default.ReactNode;
|
|
42
|
+
/** Reuse the publication's existing account and sign-in components. */
|
|
43
|
+
readerAccountUrl?: string;
|
|
44
|
+
renderSignIn?: (returnTo: string) => react__default.ReactNode;
|
|
45
|
+
feedbackEmail?: string;
|
|
42
46
|
}): react__default.JSX.Element;
|
|
43
47
|
/** Paragraph comparisons preserve readable prose and identify title/dek and media changes. */
|
|
44
48
|
declare function ReviewDiff({ before, after, }: {
|
package/dist/story-review.js
CHANGED
|
@@ -156,7 +156,18 @@ function useReviewDrafts(session, revision, initial, request) {
|
|
|
156
156
|
}
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
|
+
async function encodePendingPhoto(value) {
|
|
160
|
+
const { file, ...details } = value;
|
|
161
|
+
return { ...details, file: { bytes: await file.arrayBuffer(), name: file.name, type: file.type, lastModified: file.lastModified } };
|
|
162
|
+
}
|
|
163
|
+
function decodePendingPhoto(value) {
|
|
164
|
+
if (!value) return void 0;
|
|
165
|
+
if (value.file instanceof File) return value;
|
|
166
|
+
const file = value.file;
|
|
167
|
+
return { ...value, file: new File([file.bytes], file.name, { type: file.type, lastModified: file.lastModified }) };
|
|
168
|
+
}
|
|
159
169
|
async function stored(key, value) {
|
|
170
|
+
const encoded = value ? await encodePendingPhoto(value) : value;
|
|
160
171
|
const db = await new Promise((resolve, reject) => {
|
|
161
172
|
const request = indexedDB.open("story-review-pending-media", 1);
|
|
162
173
|
request.onupgradeneeded = () => request.result.createObjectStore("photos");
|
|
@@ -167,8 +178,14 @@ async function stored(key, value) {
|
|
|
167
178
|
return await new Promise((resolve, reject) => {
|
|
168
179
|
const tx = db.transaction("photos", value === void 0 ? "readonly" : "readwrite");
|
|
169
180
|
const store = tx.objectStore("photos");
|
|
170
|
-
const request = value === void 0 ? store.get(key) : value === null ? store.delete(key) : store.put(
|
|
171
|
-
tx.oncomplete = () =>
|
|
181
|
+
const request = value === void 0 ? store.get(key) : value === null ? store.delete(key) : store.put(encoded, key);
|
|
182
|
+
tx.oncomplete = () => {
|
|
183
|
+
try {
|
|
184
|
+
resolve(value === void 0 ? decodePendingPhoto(request.result) : void 0);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
reject(error);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
172
189
|
tx.onerror = () => reject(tx.error);
|
|
173
190
|
tx.onabort = () => reject(tx.error);
|
|
174
191
|
});
|
|
@@ -272,6 +289,23 @@ function proseDiff(before, after) {
|
|
|
272
289
|
emit("same", a.slice(endA).join(""));
|
|
273
290
|
return out;
|
|
274
291
|
}
|
|
292
|
+
|
|
293
|
+
// src/story-review-email.ts
|
|
294
|
+
function reviewEmailText(review, answers) {
|
|
295
|
+
const header = `Private story feedback \u2014 not published
|
|
296
|
+
${review.revision.document.title}
|
|
297
|
+
Preview v${review.revision.number} \xB7 Reference ${review.revision.id}
|
|
298
|
+
|
|
299
|
+
Your name:
|
|
300
|
+
|
|
301
|
+
`;
|
|
302
|
+
return header + (answers === void 0 ? "My feedback:\n" : "These questions are optional. Answer only what you would like to share.\n\n" + review.questions.map((q, i) => `${i + 1}. ${q.prompt}
|
|
303
|
+
${answers[q.id] || ""}`).join("\n\n"));
|
|
304
|
+
}
|
|
305
|
+
function reviewEmailLink(email, title, text) {
|
|
306
|
+
if (!email || !/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email)) return void 0;
|
|
307
|
+
return `mailto:${encodeURIComponent(email)}?subject=${encodeURIComponent(`Private story feedback: ${title}`)}&body=${encodeURIComponent(text)}`;
|
|
308
|
+
}
|
|
275
309
|
function EditorialNotices({ editorial }) {
|
|
276
310
|
return editorial ? /* @__PURE__ */ jsxs("aside", { "aria-label": "Editorial notices", children: [
|
|
277
311
|
editorial.content_kind && editorial.content_kind !== "EDITORIAL" && /* @__PURE__ */ jsx("p", { children: human(editorial.content_kind) }),
|
|
@@ -291,15 +325,40 @@ function StoryReview({
|
|
|
291
325
|
publicationName,
|
|
292
326
|
nameplateUrl,
|
|
293
327
|
publicationHeader,
|
|
294
|
-
publicationFooter
|
|
328
|
+
publicationFooter,
|
|
329
|
+
readerAccountUrl,
|
|
330
|
+
renderSignIn,
|
|
331
|
+
feedbackEmail
|
|
295
332
|
}) {
|
|
296
333
|
const base = `/api/news/review/${encodeURIComponent(sessionId)}`;
|
|
297
334
|
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
335
|
const [shareUrl, setShareUrl] = useState(""), [canShare, setCanShare] = useState(false);
|
|
299
336
|
const [contributorName, setContributorName] = useState(""), [contributorEmail, setContributorEmail] = useState("");
|
|
300
337
|
const [identityStep, setIdentityStep] = useState("name"), [requestedPanel, setRequestedPanel] = useState("comment");
|
|
338
|
+
const [reader, setReader] = useState(null);
|
|
339
|
+
const [readerLoading, setReaderLoading] = useState(!!readerAccountUrl);
|
|
340
|
+
const [useEmailCode, setUseEmailCode] = useState(!renderSignIn);
|
|
341
|
+
const [returnPanel, setReturnPanel] = useState("");
|
|
342
|
+
const resumed = useRef(false);
|
|
343
|
+
const [copiedQuestions, setCopiedQuestions] = useState(false);
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
if (!readerAccountUrl) return;
|
|
346
|
+
let active = true;
|
|
347
|
+
void fetch(readerAccountUrl, { credentials: "same-origin", cache: "no-store", signal: AbortSignal.timeout(15e3) }).then(async (response) => {
|
|
348
|
+
if (response.ok) {
|
|
349
|
+
const data = await response.json();
|
|
350
|
+
if (active) setReader(data.user || null);
|
|
351
|
+
}
|
|
352
|
+
}).catch(() => {
|
|
353
|
+
}).finally(() => {
|
|
354
|
+
if (active) setReaderLoading(false);
|
|
355
|
+
});
|
|
356
|
+
return () => {
|
|
357
|
+
active = false;
|
|
358
|
+
};
|
|
359
|
+
}, [readerAccountUrl]);
|
|
301
360
|
function setPanel(next) {
|
|
302
|
-
if (review?.identity_required && !["article", "history", "identity", "share"].includes(next)) {
|
|
361
|
+
if (review?.identity_required && !["article", "history", "identity", "share", "question-list"].includes(next)) {
|
|
303
362
|
setRequestedPanel(next);
|
|
304
363
|
setPanelState("identity");
|
|
305
364
|
} else setPanelState(next);
|
|
@@ -397,10 +456,41 @@ function StoryReview({
|
|
|
397
456
|
setReview(data);
|
|
398
457
|
}
|
|
399
458
|
}
|
|
459
|
+
async function connectReader(target = requestedPanel) {
|
|
460
|
+
setBusy(true);
|
|
461
|
+
setError("");
|
|
462
|
+
try {
|
|
463
|
+
await request("/account", {});
|
|
464
|
+
await load();
|
|
465
|
+
setPanelState(target);
|
|
466
|
+
setReturnPanel("");
|
|
467
|
+
const url = new URL(window.location.href);
|
|
468
|
+
url.searchParams.delete("respond");
|
|
469
|
+
window.history.replaceState(null, "", url.pathname + url.search);
|
|
470
|
+
} catch (e) {
|
|
471
|
+
setError(e.message);
|
|
472
|
+
setPanelState("identity");
|
|
473
|
+
} finally {
|
|
474
|
+
setBusy(false);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
useEffect(() => {
|
|
478
|
+
if (!returnPanel || !review || !reader?.email_verified || resumed.current) return;
|
|
479
|
+
resumed.current = true;
|
|
480
|
+
void connectReader(returnPanel);
|
|
481
|
+
}, [returnPanel, review, reader]);
|
|
400
482
|
useEffect(() => {
|
|
401
483
|
let active = true;
|
|
402
484
|
void (async () => {
|
|
403
485
|
try {
|
|
486
|
+
const returnUrl = new URL(window.location.href);
|
|
487
|
+
if (returnUrl.searchParams.get("signin") === "retry") setError("Sign-in wasn\u2019t completed. Try again or use an email code.");
|
|
488
|
+
const respond = returnUrl.searchParams.get("respond") || "";
|
|
489
|
+
if (["feedback", "changes", "questions", "media", "comment", "edits"].includes(respond)) {
|
|
490
|
+
setRequestedPanel(respond);
|
|
491
|
+
setReturnPanel(respond);
|
|
492
|
+
setPanelState("identity");
|
|
493
|
+
}
|
|
404
494
|
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
495
|
let exchange = {};
|
|
406
496
|
try {
|
|
@@ -412,7 +502,9 @@ function StoryReview({
|
|
|
412
502
|
if (address) {
|
|
413
503
|
const canonical = new URL(address, window.location.origin);
|
|
414
504
|
if (canonical.origin === window.location.origin) {
|
|
415
|
-
|
|
505
|
+
const address2 = new URL(canonical);
|
|
506
|
+
if (respond) address2.searchParams.set("respond", respond);
|
|
507
|
+
window.history.replaceState(null, "", address2.pathname + address2.search);
|
|
416
508
|
if (active) {
|
|
417
509
|
setShareUrl(canonical.href);
|
|
418
510
|
setCanShare(exchange.can_share === true);
|
|
@@ -577,6 +669,15 @@ function StoryReview({
|
|
|
577
669
|
(r) => r.id === (compareTo || review?.revision.id)
|
|
578
670
|
);
|
|
579
671
|
const errorMessage = error ? /* @__PURE__ */ jsx("p", { ref: errorRef, tabIndex: -1, role: "alert", className: "sm-review-alert", children: error }) : null;
|
|
672
|
+
const emailLink = review ? reviewEmailLink(feedbackEmail, review.revision.document.title, reviewEmailText(review)) : void 0;
|
|
673
|
+
const questionText = review ? reviewEmailText(review, answers) : "";
|
|
674
|
+
const questionsEmail = review ? reviewEmailLink(feedbackEmail, review.revision.document.title, questionText) : void 0;
|
|
675
|
+
const signInReturn = shareUrl ? new URL(shareUrl).pathname + new URL(shareUrl).search + `&respond=${encodeURIComponent(requestedPanel)}` : "";
|
|
676
|
+
const emailAlternative = emailLink && /* @__PURE__ */ jsxs("p", { className: "sm-review-email-alternative", children: [
|
|
677
|
+
"Prefer email? ",
|
|
678
|
+
/* @__PURE__ */ jsx("a", { href: emailLink, children: "Email the newsroom" }),
|
|
679
|
+
". Your email app will open; send your message there."
|
|
680
|
+
] });
|
|
580
681
|
return /* @__PURE__ */ jsxs("div", { className: `sm-review-frame${publicationHeader ? " sm-review-publication-frame" : ""}`, children: [
|
|
581
682
|
/* @__PURE__ */ jsx("div", { className: "sm-review-banner-wrap", children: /* @__PURE__ */ jsxs("div", { className: "sm-review-private", children: [
|
|
582
683
|
/* @__PURE__ */ jsx("strong", { children: review?.published ? "Private review" : "Private preview \xB7 Not published" }),
|
|
@@ -777,7 +878,7 @@ function StoryReview({
|
|
|
777
878
|
{
|
|
778
879
|
onClick: () => {
|
|
779
880
|
setMoreQuestions(false);
|
|
780
|
-
setPanel("questions");
|
|
881
|
+
setPanel(review.identity_required ? "question-list" : "questions");
|
|
781
882
|
},
|
|
782
883
|
children: "Tell Us More"
|
|
783
884
|
}
|
|
@@ -790,7 +891,8 @@ function StoryReview({
|
|
|
790
891
|
children: "Prefer Not to Publish"
|
|
791
892
|
}
|
|
792
893
|
),
|
|
793
|
-
can("COMMENT") && /* @__PURE__ */ jsx("button", { className: "sm-review-quiet", onClick: () => setPanel("comment"), children: "Leave a Comment" })
|
|
894
|
+
can("COMMENT") && /* @__PURE__ */ jsx("button", { className: "sm-review-quiet", onClick: () => setPanel("comment"), children: "Leave a Comment" }),
|
|
895
|
+
emailAlternative
|
|
794
896
|
] }),
|
|
795
897
|
/* @__PURE__ */ jsxs(
|
|
796
898
|
"nav",
|
|
@@ -813,7 +915,7 @@ function StoryReview({
|
|
|
813
915
|
can("UPLOAD_MEDIA") && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => openMedia(), children: celebration ? "Add Photos" : "Add photos" }),
|
|
814
916
|
!celebration && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
815
917
|
can("PROPOSE_EDITS") && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("edits"), children: "Suggest edits" }),
|
|
816
|
-
can("ANSWER_QUESTIONS") && review.questions.length > 0 && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("questions"), children: "Answer questions" }),
|
|
918
|
+
can("ANSWER_QUESTIONS") && review.questions.length > 0 && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel(review.identity_required ? "question-list" : "questions"), children: "Answer questions" }),
|
|
817
919
|
can("COMMENT") && /* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("comment"), children: "Leave a comment" })
|
|
818
920
|
] })
|
|
819
921
|
]
|
|
@@ -835,7 +937,10 @@ function StoryReview({
|
|
|
835
937
|
panel === "share" && /* @__PURE__ */ jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Share private preview", children: [
|
|
836
938
|
/* @__PURE__ */ jsx("h2", { children: "Share with your family" }),
|
|
837
939
|
errorMessage,
|
|
838
|
-
/* @__PURE__ */
|
|
940
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
941
|
+
"Anyone with this link can read the private preview. ",
|
|
942
|
+
renderSignIn ? "To contribute, they can continue with Google or use an email code." : "To send feedback or photos, they will enter their own name and verify their email. No account is needed."
|
|
943
|
+
] }),
|
|
839
944
|
/* @__PURE__ */ jsx("label", { htmlFor: "review-share-url", children: "Private preview link" }),
|
|
840
945
|
/* @__PURE__ */ jsx("input", { id: "review-share-url", readOnly: true, value: shareUrl, onFocus: (e) => e.currentTarget.select() }),
|
|
841
946
|
/* @__PURE__ */ jsx("button", { onClick: async () => {
|
|
@@ -848,11 +953,48 @@ function StoryReview({
|
|
|
848
953
|
}, children: "Copy private link" }),
|
|
849
954
|
/* @__PURE__ */ jsx("button", { onClick: () => setPanel("article"), children: "Back to the story" })
|
|
850
955
|
] }),
|
|
956
|
+
panel === "question-list" && /* @__PURE__ */ jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Optional story questions", children: [
|
|
957
|
+
/* @__PURE__ */ jsxs("div", { className: "sm-review-form-heading", children: [
|
|
958
|
+
/* @__PURE__ */ jsx("h2", { children: "Tell us a little more" }),
|
|
959
|
+
/* @__PURE__ */ jsx("button", { onClick: () => setPanel("article"), children: "Close" })
|
|
960
|
+
] }),
|
|
961
|
+
/* @__PURE__ */ jsx("p", { children: "Completely optional. Read the questions first, then answer online or copy them into an email. Even one answer helps." }),
|
|
962
|
+
/* @__PURE__ */ jsx("ol", { className: "sm-review-question-list", children: review.questions.filter((q, i) => moreQuestions || i < 5 || q.required).map((q) => /* @__PURE__ */ jsx("li", { children: q.prompt }, q.id)) }),
|
|
963
|
+
!moreQuestions && review.questions.length > 5 && /* @__PURE__ */ jsx("button", { onClick: () => setMoreQuestions(true), children: "See all questions" }),
|
|
964
|
+
/* @__PURE__ */ jsx("button", { className: "sm-review-primary", onClick: () => setPanel("questions"), children: "Answer online" }),
|
|
965
|
+
/* @__PURE__ */ jsx("p", { children: "Sign in to save your answers as you go. You can return whenever it suits you." }),
|
|
966
|
+
/* @__PURE__ */ jsx("button", { onClick: async () => {
|
|
967
|
+
try {
|
|
968
|
+
await navigator.clipboard.writeText(questionText);
|
|
969
|
+
setNotice("Questions copied. Paste them into an email and answer as many as you like.");
|
|
970
|
+
} catch {
|
|
971
|
+
setCopiedQuestions(true);
|
|
972
|
+
}
|
|
973
|
+
}, children: "Copy all questions" }),
|
|
974
|
+
copiedQuestions && /* @__PURE__ */ jsxs("label", { children: [
|
|
975
|
+
"Copy these questions",
|
|
976
|
+
/* @__PURE__ */ jsx("textarea", { readOnly: true, rows: 10, value: questionText, onFocus: (e) => e.currentTarget.select() })
|
|
977
|
+
] }),
|
|
978
|
+
questionsEmail && /* @__PURE__ */ jsxs("p", { children: [
|
|
979
|
+
/* @__PURE__ */ jsx("a", { href: questionsEmail, children: "Answer by email" }),
|
|
980
|
+
" \u2014 opens your email app with the questions ready."
|
|
981
|
+
] })
|
|
982
|
+
] }),
|
|
851
983
|
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" : "
|
|
984
|
+
/* @__PURE__ */ jsx("h2", { children: identityStep === "code" ? "Check your email" : "Continue to help with the story" }),
|
|
853
985
|
errorMessage,
|
|
854
|
-
/* @__PURE__ */ jsx("p", { children: "Your name
|
|
855
|
-
identityStep === "name"
|
|
986
|
+
/* @__PURE__ */ jsx("p", { children: "Your name accompanies your feedback. Your email stays private." }),
|
|
987
|
+
identityStep === "name" && renderSignIn && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
988
|
+
readerLoading ? /* @__PURE__ */ jsx("p", { role: "status", children: "Checking your sign-in\u2026" }) : reader?.email_verified ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
989
|
+
/* @__PURE__ */ jsxs("button", { className: "sm-review-primary", disabled: busy, onClick: () => void connectReader(), children: [
|
|
990
|
+
"Continue as ",
|
|
991
|
+
reader.full_name || reader.email
|
|
992
|
+
] }),
|
|
993
|
+
/* @__PURE__ */ jsx("p", { children: "Your reader account is ready. No email code is needed." })
|
|
994
|
+
] }) : renderSignIn(signInReturn),
|
|
995
|
+
!useEmailCode && /* @__PURE__ */ jsx("button", { className: "sm-review-quiet", onClick: () => setUseEmailCode(true), children: "Use an email code instead" })
|
|
996
|
+
] }),
|
|
997
|
+
identityStep === "name" && useEmailCode && /* @__PURE__ */ jsxs("form", { onSubmit: async (e) => {
|
|
856
998
|
e.preventDefault();
|
|
857
999
|
setBusy(true);
|
|
858
1000
|
setError("");
|
|
@@ -872,8 +1014,10 @@ function StoryReview({
|
|
|
872
1014
|
/* @__PURE__ */ jsx("label", { htmlFor: "contributor-email", children: "Your email" }),
|
|
873
1015
|
/* @__PURE__ */ jsx("input", { id: "contributor-email", type: "email", autoComplete: "email", value: contributorEmail, maxLength: 320, required: true, onChange: (e) => setContributorEmail(e.target.value) }),
|
|
874
1016
|
/* @__PURE__ */ jsx("p", { children: "We'll email you a code to confirm it's you." }),
|
|
1017
|
+
/* @__PURE__ */ jsx("p", { children: "No password or account is needed for this option." }),
|
|
875
1018
|
/* @__PURE__ */ jsx("button", { disabled: busy, children: busy ? "Sending your code\u2026" : "Email me a code" })
|
|
876
|
-
] })
|
|
1019
|
+
] }),
|
|
1020
|
+
identityStep === "code" && /* @__PURE__ */ jsxs("form", { onSubmit: async (e) => {
|
|
877
1021
|
e.preventDefault();
|
|
878
1022
|
setBusy(true);
|
|
879
1023
|
setError("");
|
|
@@ -909,6 +1053,7 @@ function StoryReview({
|
|
|
909
1053
|
}, children: "Send a new code" }),
|
|
910
1054
|
/* @__PURE__ */ jsx("button", { type: "button", disabled: busy, onClick: () => setIdentityStep("name"), children: "Use a different email" })
|
|
911
1055
|
] }),
|
|
1056
|
+
emailAlternative,
|
|
912
1057
|
/* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Just read the story" })
|
|
913
1058
|
] }),
|
|
914
1059
|
panel === "feedback" && /* @__PURE__ */ jsxs("section", { ref: responseRef, tabIndex: -1, className: "sm-review-form", "aria-label": "Family feedback", children: [
|
|
@@ -922,7 +1067,7 @@ function StoryReview({
|
|
|
922
1067
|
/* @__PURE__ */ jsx("button", { className: "sm-review-primary", 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.", false), children: "Send my feedback" }),
|
|
923
1068
|
/* @__PURE__ */ jsx("button", { disabled: busy, onClick: () => setPanel("article"), children: "Back to the story" })
|
|
924
1069
|
] }),
|
|
925
|
-
!["article", "identity", "share", "feedback"].includes(panel) && /* @__PURE__ */ jsxs(
|
|
1070
|
+
!["article", "identity", "share", "feedback", "question-list"].includes(panel) && /* @__PURE__ */ jsxs(
|
|
926
1071
|
"section",
|
|
927
1072
|
{
|
|
928
1073
|
ref: responseRef,
|
|
@@ -1043,7 +1188,11 @@ function StoryReview({
|
|
|
1043
1188
|
onClick: () => setMoreQuestions(true),
|
|
1044
1189
|
children: "Answer More Questions"
|
|
1045
1190
|
}
|
|
1046
|
-
)
|
|
1191
|
+
),
|
|
1192
|
+
questionsEmail && /* @__PURE__ */ jsxs("p", { children: [
|
|
1193
|
+
/* @__PURE__ */ jsx("a", { href: questionsEmail, children: "Email these answers instead" }),
|
|
1194
|
+
". Send them from your email app; your online draft stays saved."
|
|
1195
|
+
] })
|
|
1047
1196
|
]
|
|
1048
1197
|
}
|
|
1049
1198
|
),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalemule/news-publication",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.35",
|
|
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
|
@@ -630,3 +630,8 @@
|
|
|
630
630
|
.sm-review-sharing button, .sm-review-identity-status button { min-height:48px; font:inherit; padding:10px 16px; cursor:pointer; }
|
|
631
631
|
.sm-review-form input#review-share-url { width:100%; font-size:16px; min-width:0; }
|
|
632
632
|
.sm-review-form label[for^="contributor-"] { display:block; margin-top:16px; }
|
|
633
|
+
|
|
634
|
+
.sm-review-question-list { padding-left: 1.5rem; margin: 1.5rem 0; }
|
|
635
|
+
.sm-review-question-list li { padding-left: .4rem; margin: 0 0 1.2rem; line-height: 1.6; }
|
|
636
|
+
.sm-review-email-alternative { border-top: 1px solid var(--sm-review-line, #d7d7d7); padding-top: 1rem; margin-top: 1.5rem; line-height: 1.6; }
|
|
637
|
+
.sm-review-email-alternative a { font-weight: 700; text-decoration: underline; text-underline-offset: .2em; }
|