Skip to content

Commit

Permalink
Add Deadline to Copy (#207)
Browse files Browse the repository at this point in the history
* update form schema and add deadline input field

* add datepicker to form

* change to basic input

* change type of instance config

* change metohd of entry calculate deadlien in sent email

* getting dateline

* calculating deadline. todo: update copy

* adding copy changes

* messed up

* copy done

* rmv logs

* rmv date-fns

* rmv import

* change copy, correct nudge deadline

* remove after

---------

Co-authored-by: qweliant <[email protected]>
  • Loading branch information
qweliant and qweliant authored Feb 7, 2024
1 parent 0d211c6 commit e1efba3
Show file tree
Hide file tree
Showing 15 changed files with 591 additions and 51 deletions.
2 changes: 1 addition & 1 deletion core/app/c/[communitySlug]/stages/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default async function Page({ params }: Props) {
return (
<>
<div className="flex mb-16 justify-between items-center">
<h1 className="font-bold text-xl">Stages</h1> -{" "}
<h1 className="font-bold text-xl">Stages</h1>
{/* <Link href="stages/dashboard">Manage Stages</Link> */}
</div>
<StageList
Expand Down
2 changes: 2 additions & 0 deletions core/prisma/exampleCommunitySeeds/unjournal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,8 @@ export default async function main(prisma: PrismaClient, communityUUID: string)
},
titleFieldSlug: "unjournal:title",
evaluatorFieldSlug: "unjournal:evaluator",
deadlineLength: 35,
deadlineUnits: "days",
},
},
];
Expand Down
5 changes: 3 additions & 2 deletions integrations/evaluations/app/actions/evaluate/evaluate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { useForm } from "react-hook-form";
import { Button, Form, Icon, useLocalStorage, useToast } from "ui";
import { Process } from "~/lib/components/Process";
import { Research } from "~/lib/components/Research";
import { InstanceConfig } from "~/lib/types";
import { EvaluatorWhoAccepted, InstanceConfig } from "~/lib/types";
import { submit, upload } from "./actions";

type Props = {
instanceId: string;
instanceConfig: InstanceConfig;
pub: GetPubResponseBody;
pubType: GetPubTypeResponseBody;
evaluator: EvaluatorWhoAccepted;
};

export function Evaluate(props: Props) {
Expand Down Expand Up @@ -105,7 +106,7 @@ export function Evaluate(props: Props) {
url={submissionUrl}
evaluating
/>
<Process />
<Process evaluator={props.evaluator} />
<h2>{pubType.name}</h2>
<p>{pubType.description}</p>
<Form {...form}>
Expand Down
2 changes: 2 additions & 0 deletions integrations/evaluations/app/actions/evaluate/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { cookie } from "~/lib/request";
import { Declined } from "./declined";
import { Evaluate } from "./evaluate";
import { Submitted } from "./submitted";
import { EvaluatorWhoAccepted } from "~/lib/types";

type Props = {
searchParams: {
Expand Down Expand Up @@ -38,6 +39,7 @@ export default async function Page(props: Props) {
instanceConfig={instanceConfig}
pub={pub}
pubType={pubType}
evaluator={instanceState[user.id] as EvaluatorWhoAccepted}
/>
);
// If they have responded "Decline", render the decline page.
Expand Down
10 changes: 10 additions & 0 deletions integrations/evaluations/app/actions/respond/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
sendRequestedInfoNotification,
unscheduleNoReplyNotificationEmail,
unscheduleReminderEmail,
calculateDeadline,
} from "~/lib/emails";
import { getInstanceConfig, getInstanceState, setInstanceState } from "~/lib/instance";
import { cookie } from "~/lib/request";
Expand Down Expand Up @@ -38,7 +39,16 @@ export const accept = async (instanceId: string, pubId: string) => {
...evaluator,
status: "accepted",
acceptedAt: new Date().toString(),
deadline: new Date(Date.now()),
};
const deadline = calculateDeadline(
{
deadlineLength: instanceConfig.deadlineLength,
deadlineUnit: instanceConfig.deadlineUnit,
},
new Date(evaluator.acceptedAt)
);
evaluator.deadline = deadline;
await setInstanceState(instanceId, pubId, instanceState);
// Unschedule reminder email.
await unscheduleReminderEmail(instanceId, pubId, evaluator);
Expand Down
51 changes: 38 additions & 13 deletions integrations/evaluations/app/actions/respond/respond.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use client";

import { GetPubResponseBody } from "@pubpub/sdk";
import { useCallback, useEffect } from "react";
import { useCallback } from "react";
import { Button, toast } from "ui";
import { accept, contact, decline } from "./actions";
import { InstanceConfig } from "~/lib/types";
import Link from "next/link";
import { calculateDeadline } from "~/lib/emails";

type Props = {
intent: "accept" | "decline" | "info";
Expand Down Expand Up @@ -88,9 +89,10 @@ const EvaluationProcess = () => {
>
value this work
</a>
, we offer evaluators a $400 honorarium for completing the assignment by the
specified deadline, and we are also setting aside $150 per evaluation for evaluator
incentives and prizes.
, we offer evaluators a $300 honorarium for completing the assignment by the
deadline. Evaluators may earn an additional $100 “prompt evaluation bonus” for
completing the assignment promptly (see below). As of February 2024: We are also
currently setting aside $150 per evaluation for evaluator incentives and prizes.
</p>
<p>
Your evaluation will be made public and given a DOI, but you have the option to{" "}
Expand Down Expand Up @@ -206,7 +208,13 @@ export const Respond = (props: Props) => {
const submissionUrl = props.pub.values["unjournal:url"] as string;
const submissionTitle = props.pub.values[props.instanceConfig.titleFieldSlug] as string;
const submissionAbstract = props.pub.values["unjournal:description"] as string;

const deadline = calculateDeadline(
{
deadlineLength: props.instanceConfig.deadlineLength,
deadlineUnit: props.instanceConfig.deadlineUnit,
},
new Date(Date.now())
);
if (props.intent === "decline") {
const params = new URLSearchParams(window.location.search);
params.set("intent", "info");
Expand Down Expand Up @@ -280,12 +288,18 @@ export const Respond = (props: Props) => {
<>
<h2>Confirm</h2>
<p>
We encourage reviewers to complete reviews in three weeks. Upon accepting
this invitation, your evaluation will be due roughly on{" "}
We strongly encourage evaluators to complete evaluations relatively quickly,
for the benefit of authors, research-users, and the evaluation ecosystem. If
you submit the evaluation within that window (by{" "}
<strong>
{new Date(Date.now() + 21 * (1000 * 60 * 60 * 24)).toLocaleDateString()}
{new Date(
deadline.getTime() - 21 * (1000 * 60 * 60 * 24)
).toLocaleDateString()}
</strong>
.
), you will receive a $100 “prompt evaluation bonus.” After{" "}
<strong>{new Date(deadline.getTime()).toLocaleDateString()}</strong>, we
will consider re-assigning the evaluation, and later submissions may not be
eligible for the full baseline compensation.
</p>
<div className="flex gap-1">
<Button onClick={onAccept}>Accept</Button>
Expand All @@ -306,10 +320,21 @@ export const Respond = (props: Props) => {
<>
<h2>To respond to our invitation...</h2>
<p>
To agree to take on this assignment, please click the 'Accept' button below.
If you have questions at this point, please select Contact Evaluation
Manager. If you will not be able to accept our invitation, please choose
'Decline' below.
To agree to take on this assignment, please click the ‘Accept’ button below.
If you have questions at this point, please select ‘Contact Evaluation
Manager’. If you cannot accept our invitation, please choose ‘Decline’
below. We strongly encourage evaluators to complete evaluations relatively
quickly, for the benefit of authors, research-users, and the evaluation
ecosystem. If you submit the evaluation within that window (by{" "}
<strong>
{new Date(
deadline.getTime() - 21 * (1000 * 60 * 60 * 24)
).toLocaleDateString()}
</strong>
), you will receive a $100 “prompt evaluation bonus.” After{" "}
<strong>{new Date(deadline.getTime()).toLocaleDateString()}</strong>, we
will consider re-assigning the evaluation, and later submissions may not be
eligible for the full baseline compensation.
</p>
<div className="flex gap-1">
<Button onClick={onAccept}>Accept</Button>
Expand Down
63 changes: 60 additions & 3 deletions integrations/evaluations/app/configure/configure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import {
FormMessage,
Icon,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Textarea,
useToast,
} from "ui";
Expand Down Expand Up @@ -51,6 +56,9 @@ const schema: z.ZodType<InstanceConfig> = z.object({
subject: z.string(),
message: z.string(),
}),
// coerce is used here to assert this field is a number, otherwise a vlaidation error will be thrown saying this is a string
deadlineLength: z.coerce.number().min(35),
deadlineUnit: z.enum(["days", "months"]),
});

const isActionRedirect = (props: Props): props is RedirectProps => {
Expand All @@ -62,17 +70,19 @@ const defaultEmailTemplate = {
message: "Please reach out if you have any questions.",
};

const defaultInstanceConfig = {
const defaultFormValues: InstanceConfig = {
pubTypeId: "",
evaluatorFieldSlug: "",
titleFieldSlug: "",
template: defaultEmailTemplate,
emailTemplate: defaultEmailTemplate,
deadlineLength: 35,
deadlineUnit: "days",
};

export function Configure(props: Props) {
const { toast } = useToast();
const defaultValues = useMemo(
() => Object.assign({}, defaultInstanceConfig, props.instanceConfig),
() => Object.assign({}, defaultFormValues, props.instanceConfig),
[]
);
const form = useForm<z.infer<typeof schema>>({
Expand Down Expand Up @@ -166,6 +176,53 @@ export function Configure(props: Props) {
</FormItem>
)}
/>
<div className="text-xl font-medium">
<span>Deadline</span>
</div>
<FormField
control={form.control}
name="deadlineLength"
render={({ field }) => (
<FormItem>
<FormLabel>Deadline length</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
This field is used to determine thhe length of the deadline.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="deadlineUnit"
render={({ field }) => (
<FormItem>
<FormLabel>Deadline Format</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a verified email to display" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="days">days</SelectItem>
<SelectItem value="months">months</SelectItem>
</SelectContent>
</Select>
<FormDescription>
This field allows you to select whether the deadline is in
days or months.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="text-xl font-medium">
<span>Email Template</span>
</div>
Expand Down
19 changes: 18 additions & 1 deletion integrations/evaluations/lib/components/Process.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
export const Process = () => {
import { EvaluatorWhoAccepted } from "../types";

type Props = {
evaluator: EvaluatorWhoAccepted;
};

export const Process = (props: Props) => {
const deadline = new Date(props.evaluator.deadline);

return (
<>
<p>
We strongly encourage evaluators to complete evaluations relatively quickly, for the
benefit of authors, research-users, and the evaluation ecosystem. If you submit the
evaluation within that window (by{" "}
{new Date(deadline.getTime() - 21 * (1000 * 60 * 60 * 24)).toLocaleDateString()} ),
you will receive a $100 “prompt evaluation bonus.” After{" "}
{deadline.toLocaleDateString()}, we will consider re-assigning the evaluation, and
later submissions may not be eligible for the full baseline compensation.
</p>
<h2>About our evaluation process</h2>
<p>
As a sign that we{" "}
Expand Down
54 changes: 38 additions & 16 deletions integrations/evaluations/lib/emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,26 @@ import {

const DAYS_TO_ACCEPT_INVITE = 10;
const DAYS_TO_REMIND_EVALUATOR = 5;
const DAYS_TO_SUBMIT_EVALUATION = 21;

/**
* Reaturns a new date object with the deadline calculated based on the deadlineLength and deadlineUnit.
* @param deadline
* @param date
* @returns Date
*/
export function calculateDeadline(
deadline: Pick<InstanceConfig, "deadlineLength" | "deadlineUnit">,
date: Date
): Date {
switch (deadline.deadlineUnit) {
case "days":
return new Date(date.setMinutes(date.getMinutes() + deadline.deadlineLength * 24 * 60));
case "months":
return new Date(date.setMonth(date.getMonth() + deadline.deadlineLength));
default:
throw new Error('Invalid time unit. Use "days", "weeks", or "months".');
}
}

const notificationFooter =
'<p><em>This is an automated email sent from Unjournal. Please contact <a href="mailto:[email protected]">[email protected]</a> with any questions.</em></p>';
Expand Down Expand Up @@ -71,11 +90,10 @@ export const scheduleNoSubmitNotificationEmail = async (
instanceId: string,
instanceConfig: InstanceConfig,
pubId: string,
evaluator: EvaluatorWithInvite
evaluator: EvaluatorWhoAccepted
) => {
const jobKey = makeNoSubmitJobKey(instanceId, pubId, evaluator);
const runAt = new Date(evaluator.invitedAt);
runAt.setMinutes(runAt.getMinutes() + DAYS_TO_SUBMIT_EVALUATION * 24 * 60);
const runAt = evaluator.deadline;

await client.scheduleEmail(
instanceId,
Expand All @@ -95,7 +113,7 @@ ${notificationFooter}`,
},
},
extra: {
due_at: runAt.toLocaleDateString(),
due_at: evaluator.deadline.toLocaleDateString(),
manage_link: `<a href="{{instance.actions.manage}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}">Invite Evaluators page</a>`,
},
},
Expand Down Expand Up @@ -190,22 +208,26 @@ export const sendAcceptedEmail = async (
pubId: string,
evaluator: EvaluatorWhoAccepted
) => {
const dueAt = new Date(evaluator.acceptedAt);
dueAt.setMinutes(dueAt.getMinutes() + DAYS_TO_SUBMIT_EVALUATION * 24 * 60);

await client.sendEmail(instanceId, {
to: {
userId: evaluator.userId,
},
subject: `[Unjournal] Thank you for agreeing to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`,
message: `<p>Hi {{user.firstName}} {{user.lastName}},</p>
<p>Thank you for agreeing to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}" for <a href="https://unjournal.org/">The Unjournal</a>. Please submit your evaluation and ratings using {{extra.evaluate_link}}. The form includes general instructions as well as (potentially) specific considerations for this research and particular issues and priorities for this evaluation.</p>
<p>Please aim to submit your completed evaluation by {{extra.due_at}}. If you have any questions, do not hesitate to reach out to me at <a href="mailto:{{users.invitor.email}}">{{users.invitor.email}}</a>.</p>
<p>Once your evaluation has been submitted and reviewed, we will follow up with details about payment and next steps.</p>
<p>Thank you again for your important contribution to the future of science.</p>
<p>Thanks and best wishes,</p>
<p>{{users.invitor.firstName}} {{users.invitor.lastName}}</p>
<p><a href="https://unjournal.org/">Unjournal.org</a></p>`,
<p>Thank you for agreeing to evaluate "{{pubs.submission.values["${
instanceConfig.titleFieldSlug
}"]}}" for <a href="https://unjournal.org/">The Unjournal</a>. Please submit your evaluation and ratings using {{extra.evaluate_link}}. The form includes general instructions as well as (potentially) specific considerations for this research and particular issues and priorities for this evaluation.</p>
<p>We strongly encourage evaluators to complete evaluations within three weeks; relatively quick turnaround is an important part of The Unjournal model, for the benefit of authors, research-users, and the evaluation ecosystem. If you submit the evaluation within that window (by ${new Date(
evaluator.deadline.getTime() - 21 * (1000 * 60 * 60 * 24)
).toLocaleDateString()}), you will receive a $100 “prompt evaluation bonus.” After ${new Date(
evaluator.deadline.getTime()
).toLocaleDateString()}, we will consider re-assigning the evaluation, and later submissions may not be eligible for the full baseline compensation.</p>
<p>If you have any questions, do not hesitate to reach out to me at <a href="mailto:{{users.invitor.email}}">{{users.invitor.email}}</a>.</p>
<p>Once your evaluation has been submitted and reviewed, we will follow up with details about payment and next steps.</p>
<p>Thank you again for your important contribution to the future of science.</p>
<p>Thanks and best wishes,</p>
<p>{{users.invitor.firstName}} {{users.invitor.lastName}}</p>
<p><a href="https://unjournal.org/">Unjournal.org</a></p>`,
include: {
pubs: {
submission: pubId,
Expand All @@ -216,7 +238,7 @@ export const sendAcceptedEmail = async (
},
extra: {
evaluate_link: `<a href="{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}">this evaluation form</a>`,
due_at: dueAt.toLocaleDateString(),
due_at: evaluator.deadline.toLocaleDateString(),
},
});
};
Expand Down
Loading

0 comments on commit e1efba3

Please sign in to comment.