Created
October 21, 2021 19:06
-
-
Save samselikoff/89329c89189fda4bf52c18fe30fec654 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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