Skip to content

Instantly share code, notes, and snippets.

@samselikoff
Created October 21, 2021 19:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samselikoff/89329c89189fda4bf52c18fe30fec654 to your computer and use it in GitHub Desktop.
Save samselikoff/89329c89189fda4bf52c18fe30fec654 to your computer and use it in GitHub Desktop.
import Spinner from "@/components/Spinner";
import useCurrentUser from "@/hooks/use-current-user";
import useMutation from "@/hooks/use-mutation";
import { Dialog, Switch } from "@headlessui/react";
import * as Icons from "@heroicons/react/solid";
import { format, startOfWeek } from "date-fns";
import { Formik } from "formik";
import { AnimatePresence, motion } from "framer-motion";
import { gql } from "graphql-request";
import { useState, useRef } from "react";
let isTest = typeof window !== "undefined" && window.Cypress;
const DURATIONS = {
in: !isTest ? 0.4 : 0,
out: !isTest ? 0.45 : 0,
};
const EASE = [0.36, 0.66, 0.04, 1];
let startDate = startOfWeek(new Date());
let startDateString = format(startDate, "yyyy-MM-dd");
export default function GoalsForm({
weeklyGoals,
categories,
lastWeeksGoals,
onClose,
}) {
// Create nonreactive snapshots of data on first render
[categories] = useState(categories);
[weeklyGoals] = useState(weeklyGoals);
[lastWeeksGoals] = useState(lastWeeksGoals);
let currentUser = useCurrentUser();
let [saveWeeklyGoals] = useMutation(saveWeeklyGoalsMutation);
let doneButtonRef = useRef();
async function onSubmit(values) {
let goalsToUpsert = values.weeklyGoals
.filter((g) => g.selected)
.map((g) => ({
categoryId: g.categoryId,
total: +g.total,
userId: currentUser.id,
weekBeginning: startDateString,
}));
let goalIdsToDelete = values.weeklyGoals
.filter((g) => g.id && !g.selected)
.map((g) => g.id);
await saveWeeklyGoals({
goalsToUpsert,
goalIdsToDelete,
});
onClose();
}
return (
<Dialog
open={true}
as="div"
static
className="fixed inset-0 z-10"
onClose={onClose}
initialFocus={doneButtonRef}
>
<div className="flex flex-col items-end justify-center min-h-screen text-center">
<Dialog.Overlay
as={motion.div}
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { ease: EASE, duration: DURATIONS.in },
}}
exit={{
opacity: 0,
transition: { ease: EASE, duration: DURATIONS.out },
}}
className="fixed inset-0 bg-black/40"
/>
{/* Modal window */}
<motion.div
initial={{ y: "100%" }}
animate={{
y: 0,
transition: { ease: EASE, duration: DURATIONS.in },
}}
exit={{
y: "100%",
transition: { ease: EASE, duration: DURATIONS.out },
}}
className="relative flex flex-col flex-1 w-full px-1 pt-[calc(16px+env(safe-area-inset-top))] pointer-events-none "
>
<div className="flex-1 w-full px-4 pt-5 pb-8 overflow-visible text-left align-bottom bg-white shadow-xl pointer-events-auto rounded-t-2xl">
<Formik
initialValues={{
weeklyGoals: categories.map((c) => {
let matchingWeeklyGoal = weeklyGoals.find(
(g) => g.category.id === c.id
);
return {
id: matchingWeeklyGoal?.id,
categoryLabel: c.label,
categoryId: c.id,
selected: !!matchingWeeklyGoal,
total: matchingWeeklyGoal?.total || 1,
};
}),
}}
onSubmit={onSubmit}
>
{({ values, setFieldValue, handleSubmit, isSubmitting }) => (
<>
<form
className="my-2"
onSubmit={handleSubmit}
data-test="goal-form"
>
<div className="relative">
<button
onClick={onClose}
type="button"
className="absolute inset-y-0 left-0 text-blue-500"
>
Cancel
</button>
<Dialog.Title
as="h3"
className="text-lg font-medium text-center text-gray-900"
>
{weeklyGoals.length == 0 ? "Add goals" : "Edit goals"}
</Dialog.Title>
<div className="absolute inset-y-0 right-0 flex items-center">
<button
type="submit"
className={`relative font-semibold text-blue-500 ${
isSubmitting ? "pointer-events-none" : ""
}`}
disabled={isSubmitting}
ref={doneButtonRef}
>
<span
className={`${isSubmitting ? "invisible" : ""}`}
>
{weeklyGoals.length === 0 ? "Done" : "Save"}
</span>
{isSubmitting && (
<span className="absolute inset-0 flex items-center justify-center">
<Spinner style="plain" />
</span>
)}
</button>
</div>
</div>
<div className="mt-8 space-y-3">
{values.weeklyGoals.map((weeklyGoal, index) => (
<div
key={weeklyGoal.categoryId}
data-test="goal"
className={`${
weeklyGoal.selected ? "bg-gray-100" : ""
} px-3 rounded-xl transition`}
>
<label className="flex items-center py-4 text-lg">
<span>{weeklyGoal.categoryLabel}</span>
<Switch
checked={weeklyGoal.selected}
onChange={() =>
setFieldValue(
`weeklyGoals.${index}.selected`,
!weeklyGoal.selected
)
}
className={`${
weeklyGoal.selected ? "bg-blue-600" : ""
} w-6 h-6 ml-auto border-blue-600 border-2 rounded-full flex items-center justify-center`}
data-test="select"
>
{weeklyGoal.selected && (
<Icons.CheckIcon className="w-4 h-4 text-white" />
)}
</Switch>
</label>
<AnimatePresence>
{weeklyGoal.selected && (
<motion.div
className="overflow-hidden"
initial={{ height: 0 }}
animate={{
height: "auto",
transition: {
duration: isTest ? 0 : 0.4,
ease: EASE,
},
}}
exit={{
height: 0,
transition: {
duration: isTest ? 0 : 0.3,
ease: EASE,
},
}}
transition={{ duration: 1 }}
>
<div className="flex items-center justify-between pt-1 pb-4">
<p>
{weeklyGoal.total}{" "}
{weeklyGoal.total > 1 ? "times" : "time"}{" "}
per week
</p>
<div className="px-2 pt-2 -ml-2">
<input
type="range"
min="1"
value={weeklyGoal.total}
onChange={(e) =>
setFieldValue(
`weeklyGoals.${index}.total`,
e.target.value
)
}
max="7"
data-test="frequency"
/>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
))}
</div>
</form>
{lastWeeksGoals.length > 0 && (
<div className="mt-8">
<button
onClick={() => {
setFieldValue(
"weeklyGoals",
categories.map((c) => {
let matchingWeeklyGoal = lastWeeksGoals.find(
(g) => g.categoryId === c.id
);
return {
categoryLabel: c.label,
categoryId: c.id,
selected: !!matchingWeeklyGoal,
total: matchingWeeklyGoal?.total || 1,
};
})
);
}}
className="w-full p-3 text-left text-blue-600 rounded bg-gray-50"
>
Copy last week's goals
</button>
</div>
)}
</>
)}
</Formik>
</div>
</motion.div>
</div>
</Dialog>
);
}
let saveWeeklyGoalsMutation = gql`
mutation UpdateGoalsMutation(
$goalsToUpsert: [weeklyGoals_insert_input!]!
$goalIdsToDelete: [Int!]!
) {
insert_weeklyGoals(
objects: $goalsToUpsert
on_conflict: {
constraint: weeklyGoals_weekBeginning_categoryId_userId_key
update_columns: [total]
}
) {
returning {
id
}
}
delete_weeklyGoals(where: { id: { _in: $goalIdsToDelete } }) {
returning {
id
}
}
}
`;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment