From e73771c5fd09ffddea67f45194e61b4169c6e8ed Mon Sep 17 00:00:00 2001 From: cade Date: Sun, 7 Jul 2024 13:02:57 -0700 Subject: [PATCH] subject links, module collapse, design updates --- app/(pages)/(with-nav)/inputs/layout.tsx | 4 +- .../subjects/(list)/@archived/page.tsx | 2 +- .../(with-nav)/subjects/(list)/layout.tsx | 4 +- app/(pages)/(with-nav)/templates/layout.tsx | 4 +- .../insights/[insightId]/edit/page.tsx | 2 +- .../[subjectId]/insights/create/page.tsx | 4 +- app/(pages)/@modal/(md)/account/layout.tsx | 2 +- .../create/from-input/[inputId]/page.tsx | 4 +- .../@modal/(md)/inputs/create/page.tsx | 4 +- .../[missionId]/sessions/loading.tsx | 6 +- .../event-types/[eventTypeId]/edit/page.tsx | 12 +- .../[subjectId]/event-types/create/page.tsx | 5 +- .../[order]/from-session/[sessionId]/page.tsx | 2 +- .../sessions/create/[order]/page.tsx | 2 +- .../training-plans/create/page.tsx | 4 +- .../@modal/(md)/subjects/create/page.tsx | 4 +- .../from-event-type/[eventTypeId]/page.tsx | 4 +- .../@modal/(md)/templates/create/page.tsx | 4 +- app/_components/account-email-form.tsx | 3 +- app/_components/account-password-form.tsx | 3 +- app/_components/account-profile-form.tsx | 3 +- app/_components/alert.tsx | 2 +- app/_components/calendar.tsx | 2 +- app/_components/checkbox.tsx | 6 +- .../collapsible-archive.tsx | 0 app/_components/collapsible-section.tsx | 53 +++ app/_components/disclosure.tsx | 40 -- app/_components/dropdown-menu.tsx | 2 +- app/_components/event-card.tsx | 65 +-- app/_components/event-form.tsx | 318 +++++++------- app/_components/event-page.tsx | 48 ++- app/_components/event-type-form.tsx | 120 +++--- app/_components/event-type-menu.tsx | 2 +- app/_components/event-types.tsx | 72 +--- app/_components/form-banner.tsx | 140 ------- app/_components/input-form.tsx | 394 +++++++++--------- app/_components/input.tsx | 4 +- app/_components/insight-form.tsx | 285 +++++++------ app/_components/insight-menu.tsx | 4 +- app/_components/insight-page.tsx | 11 +- app/_components/insights.tsx | 10 +- app/_components/mission-form.tsx | 1 - app/_components/missions.tsx | 34 +- app/_components/module-card.tsx | 96 +++++ app/_components/module-form-section.tsx | 14 +- app/_components/page-modal-header.tsx | 14 +- app/_components/page-modal-loading.tsx | 4 +- app/_components/page-modal.tsx | 4 +- app/_components/popover.tsx | 3 +- app/_components/rich-textarea.tsx | 4 +- app/_components/select.tsx | 6 +- app/_components/session-form.tsx | 38 +- app/_components/session-layout.tsx | 6 +- app/_components/session-page.tsx | 41 +- app/_components/sessions-page.tsx | 12 +- app/_components/subject-form.tsx | 91 +++- app/_components/subject-layout.tsx | 84 +++- app/_components/subject-menu.tsx | 2 +- app/_components/template-form.tsx | 88 ++-- app/_components/tip.tsx | 2 +- app/_components/unsaved-changes-banner.tsx | 37 ++ app/_components/view-all-sessions-button.tsx | 1 + app/_mutations/upsert-session.ts | 3 + app/_mutations/upsert-subject.ts | 8 +- app/_queries/get-event.ts | 3 +- app/_queries/get-subject.ts | 4 +- app/_types/subject-data-json.ts | 4 + bun.lockb | Bin 358776 -> 358798 bytes package.json | 8 +- ...20240702191552_add-subject-data-column.sql | 22 + ...3_add-mission-name-to-public-get-event.sql | 112 +++++ 71 files changed, 1298 insertions(+), 1108 deletions(-) rename app/{_queries => _components}/collapsible-archive.tsx (100%) create mode 100644 app/_components/collapsible-section.tsx delete mode 100644 app/_components/disclosure.tsx delete mode 100644 app/_components/form-banner.tsx create mode 100644 app/_components/module-card.tsx create mode 100644 app/_components/unsaved-changes-banner.tsx create mode 100644 app/_types/subject-data-json.ts create mode 100644 supabase/migrations/20240702191552_add-subject-data-column.sql create mode 100644 supabase/migrations/20240707230443_add-mission-name-to-public-get-event.sql diff --git a/app/(pages)/(with-nav)/inputs/layout.tsx b/app/(pages)/(with-nav)/inputs/layout.tsx index daeeedf6..29517454 100644 --- a/app/(pages)/(with-nav)/inputs/layout.tsx +++ b/app/(pages)/(with-nav)/inputs/layout.tsx @@ -1,4 +1,5 @@ import Button from '@/_components/button'; +import PlusIcon from '@heroicons/react/24/outline/PlusIcon'; import { ReactNode } from 'react'; interface LayoutProps { @@ -10,7 +11,8 @@ const Layout = ({ children }: LayoutProps) => (

Inputs

{children} diff --git a/app/(pages)/(with-nav)/subjects/(list)/@archived/page.tsx b/app/(pages)/(with-nav)/subjects/(list)/@archived/page.tsx index 284eba6c..a09cd00e 100644 --- a/app/(pages)/(with-nav)/subjects/(list)/@archived/page.tsx +++ b/app/(pages)/(with-nav)/subjects/(list)/@archived/page.tsx @@ -1,5 +1,5 @@ +import CollapsibleArchive from '@/_components/collapsible-archive'; import SubjectList from '@/_components/subject-list'; -import CollapsibleArchive from '@/_queries/collapsible-archive'; import countArchivedSubjects from '@/_queries/count-archived-subjects'; const Page = async () => { diff --git a/app/(pages)/(with-nav)/subjects/(list)/layout.tsx b/app/(pages)/(with-nav)/subjects/(list)/layout.tsx index 60a6ca7c..8495217c 100644 --- a/app/(pages)/(with-nav)/subjects/(list)/layout.tsx +++ b/app/(pages)/(with-nav)/subjects/(list)/layout.tsx @@ -1,4 +1,5 @@ import Button from '@/_components/button'; +import PlusIcon from '@heroicons/react/24/outline/PlusIcon'; import { ReactNode } from 'react'; interface LayoutProps { @@ -11,7 +12,8 @@ const Layout = async ({ archived, children }: LayoutProps) => (

Subjects

diff --git a/app/(pages)/(with-nav)/templates/layout.tsx b/app/(pages)/(with-nav)/templates/layout.tsx index 0b21d4c3..382b97fd 100644 --- a/app/(pages)/(with-nav)/templates/layout.tsx +++ b/app/(pages)/(with-nav)/templates/layout.tsx @@ -1,4 +1,5 @@ import Button from '@/_components/button'; +import PlusIcon from '@heroicons/react/24/outline/PlusIcon'; import { ReactNode } from 'react'; interface LayoutProps { @@ -10,7 +11,8 @@ const Layout = ({ children }: LayoutProps) => (

Templates

{children} diff --git a/app/(pages)/@modal/(lg)/subjects/[subjectId]/insights/[insightId]/edit/page.tsx b/app/(pages)/@modal/(lg)/subjects/[subjectId]/insights/[insightId]/edit/page.tsx index c8988865..0ba7cd1e 100644 --- a/app/(pages)/@modal/(lg)/subjects/[subjectId]/insights/[insightId]/edit/page.tsx +++ b/app/(pages)/@modal/(lg)/subjects/[subjectId]/insights/[insightId]/edit/page.tsx @@ -26,7 +26,7 @@ const Page = async ({ params: { insightId, subjectId } }: PageProps) => { return ( <> - + ); diff --git a/app/(pages)/@modal/(lg)/subjects/[subjectId]/insights/create/page.tsx b/app/(pages)/@modal/(lg)/subjects/[subjectId]/insights/create/page.tsx index 40223514..371d264e 100644 --- a/app/(pages)/@modal/(lg)/subjects/[subjectId]/insights/create/page.tsx +++ b/app/(pages)/@modal/(lg)/subjects/[subjectId]/insights/create/page.tsx @@ -11,7 +11,7 @@ interface PageProps { } export const metadata = { - title: formatTitle(['Subjects', 'Insights', 'Create']), + title: formatTitle(['Subjects', 'Insights', 'New']), }; const Page = async ({ params: { subjectId } }: PageProps) => { @@ -24,7 +24,7 @@ const Page = async ({ params: { subjectId } }: PageProps) => { return ( <> - + ); diff --git a/app/(pages)/@modal/(md)/account/layout.tsx b/app/(pages)/@modal/(md)/account/layout.tsx index b68be168..7d94822b 100644 --- a/app/(pages)/@modal/(md)/account/layout.tsx +++ b/app/(pages)/@modal/(md)/account/layout.tsx @@ -9,7 +9,7 @@ interface LayoutProps { const Layout = async ({ children }: LayoutProps) => ( <> -
+
+ + {content} + + ); +}; + +export default CollapsibleSection; diff --git a/app/_components/disclosure.tsx b/app/_components/disclosure.tsx deleted file mode 100644 index b05d771f..00000000 --- a/app/_components/disclosure.tsx +++ /dev/null @@ -1,40 +0,0 @@ -'use client'; - -import DirtyHtml from '@/_components/dirty-html'; -import ChevronDownIcon from '@heroicons/react/24/outline/ChevronDownIcon'; -import { useToggle } from '@uidotdev/usehooks'; -import { twMerge } from 'tailwind-merge'; - -interface DisclosureProps { - children: string; - className?: string; - disabled?: boolean; -} - -const Disclosure = ({ children, className, disabled }: DisclosureProps) => { - const [disclosure, toggleDisclosure] = useToggle(disabled); - - return ( -
{ - if (!disabled && (e.target as HTMLElement).localName !== 'a') { - toggleDisclosure(); - } - }} - role="button" - > - {children} - {!disclosure && ( - - )} -
- ); -}; - -export default Disclosure; diff --git a/app/_components/dropdown-menu.tsx b/app/_components/dropdown-menu.tsx index 6c998b2f..1f5bd156 100644 --- a/app/_components/dropdown-menu.tsx +++ b/app/_components/dropdown-menu.tsx @@ -26,7 +26,7 @@ const Content = React.forwardRef< ref={ref} sideOffset={4} className={twMerge( - 'z-10 w-60 rounded border border-alpha-1 bg-bg-3 py-1 shadow-lg', + 'z-10 w-60 rounded border border-alpha-1 bg-bg-3 py-1 drop-shadow-xl', className, )} {...props} diff --git a/app/_components/event-card.tsx b/app/_components/event-card.tsx index 3a1612c3..2f319388 100644 --- a/app/_components/event-card.tsx +++ b/app/_components/event-card.tsx @@ -1,9 +1,6 @@ -import Avatar from '@/_components/avatar'; -import Disclosure from '@/_components/disclosure'; +import DirtyHtml from '@/_components/dirty-html'; import { GetEventData } from '@/_queries/get-event'; import { GetEventTypeWithInputsAndOptionsData } from '@/_queries/get-event-type-with-inputs-and-options'; -import { GetMissionWithSessionsData } from '@/_queries/get-mission-with-sessions'; -import { GetSessionWithDetailsData } from '@/_queries/get-session-with-details'; import forceArray from '@/_utilities/force-array'; import { User } from '@supabase/supabase-js'; import EventCommentForm from './event-comment-form'; @@ -11,88 +8,46 @@ import EventComments, { EventCommentsProps } from './event-comments'; import EventForm from './event-form'; interface EventCardProps { - disabled?: boolean; - event?: - | NonNullable - | NonNullable['modules'][0]['event'][0]; + event?: NonNullable; eventType: | NonNullable['type']> - | NonNullable - | NonNullable['modules'][0]; - hideContent?: boolean; + | NonNullable; isArchived?: boolean; isPublic?: boolean; isTeamMember?: boolean; - mission?: NonNullable; subjectId: string; - totalModules?: number; user?: User | null; } const EventCard = ({ - disabled, event, eventType, - hideContent, isArchived, isPublic, isTeamMember, - mission, subjectId, - totalModules, user, }: EventCardProps) => { const comments = forceArray(event?.comments); - const showDescription = !hideContent && !!eventType.content; - const showModule = mission && typeof eventType.order === 'number'; return ( - <> - {(showModule || event || showDescription) && ( -
- {(showModule || event) && ( -
- {showModule && ( - <> - Module {(eventType.order as number) + 1} / {totalModules} - - )} - {event && ( -
- {mission ? 'Completed' : 'Recorded'} by - - - {event.profile?.first_name} {event.profile?.last_name} - -
- )} -
- )} - {showDescription && ( - - {eventType.content as string} - - )} -
+
+ {eventType.content && ( + + {eventType.content} + )} {(event || (!isPublic && !isArchived)) && ( )} {event && (!!comments.length || (!isPublic && !isArchived)) && ( -
+
}
)} - +
); }; diff --git a/app/_components/event-form.tsx b/app/_components/event-form.tsx index fbfecc66..6ed70803 100644 --- a/app/_components/event-form.tsx +++ b/app/_components/event-form.tsx @@ -22,10 +22,24 @@ import { useRouter } from 'next/navigation'; import { useEffect, useRef, useTransition } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { PropsValue } from 'react-select'; +import { twMerge } from 'tailwind-merge'; import EventSelect from './event-select'; import EventStopwatch from './event-stopwatch'; +interface EventFormValues { + comment: string; + completionTime: string; + inputs: Array< + | DurationInputType + | MultiSelectInputType + | SelectInputType + | boolean + | string + >; +} + interface EventFormProps { + className?: string; disabled?: boolean; event?: | NonNullable @@ -40,19 +54,8 @@ interface EventFormProps { subjectId: string; } -interface EventFormValues { - comment: string; - completionTime: string; - inputs: Array< - | DurationInputType - | MultiSelectInputType - | SelectInputType - | boolean - | string - >; -} - const EventForm = ({ + className, disabled, event, eventType, @@ -136,7 +139,7 @@ const EventForm = ({ return ( {}} - className="border-t border-alpha-1" + className={twMerge('flex flex-col gap-6 px-4 sm:px-8', className)} onSubmit={form.handleSubmit((values) => startTransition(async () => { pendingComment.current = values.comment; @@ -167,163 +170,158 @@ const EventForm = ({ }), )} > -
- { - const today = new Date(); - const tomorrow = new Date(today); - tomorrow.setDate(tomorrow.getDate() + 1); - return tomorrow; - })(), - )} - required - step="any" - type="datetime-local" - {...form.register('completionTime')} - /> - {eventType.inputs.map(({ input }, i) => { - const id = `${eventType.id}-inputs-${i}`; - - return ( -
- {input?.type === InputType.Checkbox && ( - - )} - {input?.type === InputType.Duration && ( -
- {input.label} -
- ( - field.onChange(value)} - options={Array.from({ length: 60 }, (_, i) => ({ - id: String(i), - label: `${i}m`, - }))} - placeholder="Minutes" - value={field.value as PropsValue} - /> - )} - /> - ( - - )} - {(input?.type === InputType.MultiSelect || - input?.type === InputType.Select) && ( - ( - - )} - /> - )} - {input?.type === InputType.Stopwatch && ( - - form={form} - input={input} - inputIndex={i} - /> - )} -
- ); - })} - {!event && ( - } - /> + { + const today = new Date(); + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + return tomorrow; + })(), )} -
+ required + step="any" + type="datetime-local" + {...form.register('completionTime')} + /> + {eventType.inputs.map(({ input }, i) => { + const id = `${eventType.id}-inputs-${i}`; + + return ( +
+ {input?.type === InputType.Checkbox && ( + + )} + {input?.type === InputType.Duration && ( +
+ {input.label} +
+ ( + field.onChange(value)} + options={Array.from({ length: 60 }, (_, i) => ({ + id: String(i), + label: `${i}m`, + }))} + placeholder="Minutes" + value={field.value as PropsValue} + /> + )} + /> + ( + + )} + {(input?.type === InputType.MultiSelect || + input?.type === InputType.Select) && ( + ( + + )} + /> + )} + {input?.type === InputType.Stopwatch && ( + + form={form} + input={input} + inputIndex={i} + /> + )} +
+ ); + })} + {!event && ( + } + /> + )} {form.formState.errors.root && ( -
+
{form.formState.errors.root.message}
)} - {!isPublic && !isArchived && ( -
+ {!isPublic && !isArchived && (!event || form.formState.isDirty) && ( +
{!event && !isMission && ( Close )} - + {(!event || form.formState.isDirty) && ( + + )}
)} diff --git a/app/_components/event-page.tsx b/app/_components/event-page.tsx index 3c379682..8edda74f 100644 --- a/app/_components/event-page.tsx +++ b/app/_components/event-page.tsx @@ -1,3 +1,4 @@ +import Avatar from '@/_components/avatar'; import BackButton from '@/_components/back-button'; import Button from '@/_components/button'; import EventCard from '@/_components/event-card'; @@ -28,18 +29,43 @@ const EventPage = async ({ eventId, isPublic, subjectId }: EventPageProps) => { return ( <> - View full session - - - ) + subtitle={ + <> + {event && ( +
+ {event.type.session ? 'Completed' : 'Recorded'} by + + + {event.profile?.first_name} {event.profile?.last_name} + +
+ )} + {event.type.session && ( +
+ + Session {Number(event.type.session.order) + 1} + + + Module {Number(event.type.order) + 1} + + +
+ )} + } - title={event.type.name} + title={event.type.name ?? event.type.session?.mission?.name} /> ; - availableTemplates: NonNullable; eventType?: NonNullable; subjects: NonNullable; subjectId: string; @@ -37,7 +35,6 @@ type EventTypeFormValues = { const EventTypeForm = ({ availableInputs, - availableTemplates, eventType, subjects, subjectId, @@ -64,6 +61,7 @@ const EventTypeForm = ({ return ( <>
startTransition(async () => { const res = await upsertEventType( @@ -80,71 +78,62 @@ const EventTypeForm = ({ router.back(); }), )} - className="divide-y divide-alpha-1" > - - availableInputs={availableInputs} - availableTemplates={availableTemplates} - form={form} + + ( + + )} + /> + ( + - ( - - )} - /> - ( - 'No templates.'} - onChange={(t) => { - const template = ( - t as NonNullable[0] - ).data as TemplateDataJson; - - const inputs = availableInputs.filter(({ id }) => - forceArray(template?.inputIds).includes(id), - ) as PathValue; - - form.setValue( - 'content' as Path, - template?.content as PathValue>, - { shouldDirty: true }, - ); - - form.setValue( - 'inputs' as Path, - inputs as PathValue>, - { shouldDirty: true }, - ); - - toggleUseTemplateModal(); - }} - options={availableTemplates} - placeholder="Select a template…" - value={null} - /> - - -
-
- - )} -
- ); -}; - -export default FormBanner; diff --git a/app/_components/input-form.tsx b/app/_components/input-form.tsx index 5d7f63bd..6e89c39d 100644 --- a/app/_components/input-form.tsx +++ b/app/_components/input-form.tsx @@ -3,10 +3,10 @@ import BackButton from '@/_components/back-button'; import Button from '@/_components/button'; import Checkbox from '@/_components/checkbox'; -import FormBanner from '@/_components/form-banner'; import IconButton from '@/_components/icon-button'; import Input from '@/_components/input'; import Select, { IOption } from '@/_components/select'; +import UnsavedChangesBanner from '@/_components/unsaved-changes-banner'; import INPUT_LABELS from '@/_constants/constant-input-labels'; import InputType from '@/_constants/enum-input-type'; import useCachedForm from '@/_hooks/use-cached-form'; @@ -88,7 +88,7 @@ const InputForm = ({ return ( startTransition(async () => { @@ -116,223 +116,208 @@ const InputForm = ({ ), )} > - {!disableCache && form={form} />} -
- ( - - ( - 'No subjects.'} + onBlur={field.onBlur} + onChange={(value) => field.onChange(value)} + options={subjects as IOption[]} + placeholder="All subjects…" + tooltip={ + <> + If this input isn’t applicable to all of your subjects, + you can specify the relevant subjects here. + + } + value={field.value as PropsValue} + /> + )} + /> + + ( + { - if (e.key === 'Backspace' && !field.value) { - e.preventDefault(); - optionsArray.remove(optionIndex); + } + }} + options={INPUT_TYPE_OPTIONS} + placeholder="Select type…" + required + value={field.value as PropsValue} + /> + )} + /> + {(type === InputType.Select || type === InputType.MultiSelect) && ( + <> +
+ Options +
+ {!!optionsArray.fields.length && ( +
    + {optionsArray.fields.map((option, optionIndex) => ( +
  • + ( + { + if (e.key === 'Backspace' && !field.value) { + e.preventDefault(); + optionsArray.remove(optionIndex); - form.setFocus( - `options.${optionIndex - 1}.label`, - { - shouldSelect: true, - }, - ); - } + form.setFocus( + `options.${optionIndex - 1}.label`, + { + shouldSelect: true, + }, + ); + } - if (e.key === 'Enter') { - e.preventDefault(); + if (e.key === 'Enter') { + e.preventDefault(); - optionsArray.insert(optionIndex + 1, { - input_id: input?.id ?? '', - label: '', - order: optionIndex + 1, - }); - } - }} - placeholder="Label…" - required - right={ - } - label="Delete option" - onClick={() => - optionsArray.remove(optionIndex) - } - tabIndex={-1} - /> - } - {...field} + optionsArray.insert(optionIndex + 1, { + input_id: input?.id ?? '', + label: '', + order: optionIndex + 1, + }); + } + }} + placeholder="Label…" + required + right={ + } + label="Delete option" + onClick={() => optionsArray.remove(optionIndex)} + tabIndex={-1} /> - )} + } + {...field} /> -
  • - ))} -
- )} - -
-
- - Enable this when you don’t know all possible options - in advance. - + )} + /> + + ))} + + )} +
+ > + + Add option + +
+ + + Enable this when you don’t know all possible options in + advance. + + } + {...form.register('settings.isCreatable')} + /> + + )} + {type === InputType.Number && ( + <> +
+ + +
+
+ + Step is the interval between values. For example, if you set + the step to 5, you can only enter values that are multiples of + 5 (5, 10, 15, etc). + + } + type="number" + {...form.register('settings.step')} + /> +
+ )} {form.formState.errors.root && ( -
- {form.formState.errors.root.message} -
+
{form.formState.errors.root.message}
)} -
+
+ {!disableCache && form={form} />} ); }; diff --git a/app/_components/input.tsx b/app/_components/input.tsx index 4e1d20bf..2f3cf2d0 100644 --- a/app/_components/input.tsx +++ b/app/_components/input.tsx @@ -35,7 +35,9 @@ const Input = forwardRef( )} {tooltip && ( - {tooltip} + + {tooltip} + )}
{ const form = useCachedForm(cacheKey, { defaultValues: { - barInterval: config?.barInterval ?? BarInterval.Week, + barInterval: config?.barInterval ?? BarInterval.Day, barReducer: config?.barReducer ?? BarReducer.Mean, includeEventsFrom: config?.includeEventsFrom ?? null, includeEventsSince: config?.includeEventsSince ?? null, @@ -83,7 +85,7 @@ const InsightForm = ({ events, insight, subjectId }: InsightFormProps) => { marginBottom: config?.marginBottom ?? '60', marginLeft: config?.marginLeft ?? '60', marginRight: config?.marginRight ?? '40', - marginTop: config?.marginTop ?? '25', + marginTop: config?.marginTop ?? '30', name: insight?.name ?? '', showBars: config?.showBars ?? false, showDots: config?.showDots ?? true, @@ -135,6 +137,7 @@ const InsightForm = ({ events, insight, subjectId }: InsightFormProps) => { return (
startTransition(async () => { const res = await upsertInsight( @@ -152,7 +155,7 @@ const InsightForm = ({ events, insight, subjectId }: InsightFormProps) => { }), )} > -
+
{ />
-
- { - let value; - - if (field.value) { - for (const group of eventTypeOrTrainingPlanOptions) { - value = group.options.find((o) => o.id === field.value); - if (value) break; - } - } - - return ( - field.onChange((value as IOption)?.id)} - options={INCLUDE_EVENTS_SINCE_OPTIONS} - placeholder="The beginning of time…" - value={INCLUDE_EVENTS_SINCE_OPTIONS.find( - (o) => field.value === o.id, - )} - /> - )} - /> -
-
- -
-
- + - - + onShowBarsOrInputChange({ inputId, showBars: e.target.checked }), + })} /> -
-
+ { /> )} /> -
-
- + + { + let value; + + if (field.value) { + for (const group of eventTypeOrTrainingPlanOptions) { + value = group.options.find((o) => o.id === field.value); + if (value) break; + } + } + + return ( + field.onChange((value as IOption)?.id)} + options={INCLUDE_EVENTS_SINCE_OPTIONS} + placeholder="The beginning of time…" + value={INCLUDE_EVENTS_SINCE_OPTIONS.find( + (o) => field.value === o.id, + )} + /> + )} /> - - onShowBarsOrInputChange({ inputId, showBars: e.target.checked }), - })} + + + - + + + +
+
+ +
{form.formState.errors.root && ( -
- {form.formState.errors.root.message} -
+
{form.formState.errors.root.message}
)} -
- - Close - - +
+ form={form} /> +
+ + Close + + +
); diff --git a/app/_components/insight-menu.tsx b/app/_components/insight-menu.tsx index 153c969d..cf5062bc 100644 --- a/app/_components/insight-menu.tsx +++ b/app/_components/insight-menu.tsx @@ -20,14 +20,14 @@ const InsightMenu = ({ insightId, subjectId }: InsightMenuProps) => { <> +
} > - + - -
+
+
+
- +
); }; diff --git a/app/_components/insights.tsx b/app/_components/insights.tsx index ccf1b890..717d2a8f 100644 --- a/app/_components/insights.tsx +++ b/app/_components/insights.tsx @@ -40,14 +40,14 @@ const Insights = ({ return (
-
-
+
+
-
-
{ return (
startTransition(async () => { const res = await upsertMission( diff --git a/app/_components/missions.tsx b/app/_components/missions.tsx index 22c9e20e..fce66340 100644 --- a/app/_components/missions.tsx +++ b/app/_components/missions.tsx @@ -1,9 +1,7 @@ import Button from '@/_components/button'; import MissionMenu from '@/_components/mission-menu'; -import Tip from '@/_components/tip'; import listSubjectMissions from '@/_queries/list-subject-missions'; import ArrowUpRightIcon from '@heroicons/react/24/outline/ArrowUpRightIcon'; -import PlusIcon from '@heroicons/react/24/outline/PlusIcon'; import { ReactElement } from 'react'; import { twMerge } from 'tailwind-merge'; @@ -63,36 +61,12 @@ const Missions = async ({ isTeamMember, subjectId }: MissionsProps) => { return acc; }, [] as ReactElement[]); - if (!listItems.length && !isTeamMember) return null; + if (!listItems.length) return null; return ( -
- {isTeamMember && ( -
- - {!listItems.length && ( - - Training plans are comprised of sessions to be completed over - time. For example: “Reduce separation anxiety” or - “Stop screaming” - - )} -
- )} - {!!listItems.length && ( -
    - {listItems} -
- )} -
+
    + {listItems} +
); }; diff --git a/app/_components/module-card.tsx b/app/_components/module-card.tsx new file mode 100644 index 00000000..de861b2f --- /dev/null +++ b/app/_components/module-card.tsx @@ -0,0 +1,96 @@ +import Avatar from '@/_components/avatar'; +import CollapsibleSection from '@/_components/collapsible-section'; +import DirtyHtml from '@/_components/dirty-html'; +import { GetMissionWithSessionsData } from '@/_queries/get-mission-with-sessions'; +import { GetSessionWithDetailsData } from '@/_queries/get-session-with-details'; +import forceArray from '@/_utilities/force-array'; +import { User } from '@supabase/supabase-js'; +import EventCommentForm from './event-comment-form'; +import EventComments, { EventCommentsProps } from './event-comments'; +import EventForm from './event-form'; + +interface ModuleCardProps { + disabled?: boolean; + event?: NonNullable['modules'][0]['event'][0]; + eventType: NonNullable['modules'][0]; + isArchived?: boolean; + isPublic?: boolean; + isTeamMember?: boolean; + mission: NonNullable; + subjectId: string; + user?: User | null; +} + +const ModuleCard = ({ + disabled, + event, + eventType, + isArchived, + isPublic, + isTeamMember, + mission, + subjectId, + user, +}: ModuleCardProps) => { + const comments = forceArray(event?.comments); + + return ( + +
+ Module {(eventType.order as number) + 1} +
+ {event && ( +
+ Completed by + + + {event.profile?.first_name} {event.profile?.last_name} + +
+ )} +
+ } + titleClassName="sm:pl-8 border-0 my-0 pr-6 sm:pr-10" + > + {eventType.content && ( + + {eventType.content} + + )} + {(event || (!isPublic && !isArchived)) && ( + + )} + {event && (!!comments.length || (!isPublic && !isArchived)) && ( +
+ + {!isPublic && !isArchived && } +
+ )} + + ); +}; + +export default ModuleCard; diff --git a/app/_components/module-form-section.tsx b/app/_components/module-form-section.tsx index 7e669b90..7d57aaea 100644 --- a/app/_components/module-form-section.tsx +++ b/app/_components/module-form-section.tsx @@ -101,7 +101,7 @@ const ModuleFormSection = >({
  • >({ }} > - Create template + New template >({
    - + Use template Selecting a template will overwrite any existing module values. @@ -257,10 +257,10 @@ const ModuleFormSection = >({
    - + setCreateTemplateModal(null)} - title="Create template" + title="New template" /> >({
    - + setCreateInputModal(null)} - title="Create input" + title="New input" /> void; - title: string | null; + subtitle?: ReactNode; + title?: ReactNode; } const PageModalHeader = ({ className, - link, onClose, + subtitle, title, }: PageModalHeaderProps) => (
    - {title &&

    {title}

    } - {link} + {title &&

    {title}

    } + {subtitle}
    } onClick={onClose} /> diff --git a/app/_components/page-modal-loading.tsx b/app/_components/page-modal-loading.tsx index c0d56dbc..e9d9b895 100644 --- a/app/_components/page-modal-loading.tsx +++ b/app/_components/page-modal-loading.tsx @@ -6,10 +6,10 @@ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'; const PageModalLoading = () => ( <> -
    +
    } />
    diff --git a/app/_components/page-modal.tsx b/app/_components/page-modal.tsx index 30dbc49b..53fb9fae 100644 --- a/app/_components/page-modal.tsx +++ b/app/_components/page-modal.tsx @@ -16,7 +16,7 @@ const PageModal = ({ children, className }: PageModalProps) => { return ( -
    +
    {
    diff --git a/app/_components/popover.tsx b/app/_components/popover.tsx index 0242e924..13c8ecc2 100644 --- a/app/_components/popover.tsx +++ b/app/_components/popover.tsx @@ -13,9 +13,10 @@ const PopoverContent = forwardRef< ref={ref} align={align} className={twMerge( - 'rounded border border-alpha-1 bg-bg-3 shadow-lg', + 'rounded border border-alpha-1 bg-bg-3 drop-shadow', className, )} + onOpenAutoFocus={(e) => e.preventDefault()} sideOffset={8} {...props} /> diff --git a/app/_components/rich-textarea.tsx b/app/_components/rich-textarea.tsx index dd7bb6cc..a27ecb3a 100644 --- a/app/_components/rich-textarea.tsx +++ b/app/_components/rich-textarea.tsx @@ -160,7 +160,9 @@ const RichTextarea = ( )} {tooltip && ( - {tooltip} + + {tooltip} + )}
    {editor ? ( diff --git a/app/_components/select.tsx b/app/_components/select.tsx index 50b9b1f0..1b463ef2 100644 --- a/app/_components/select.tsx +++ b/app/_components/select.tsx @@ -132,7 +132,7 @@ const Menu = ({ ...props }: MenuProps) => (
    @@ -336,7 +336,9 @@ const Select = ( )} {tooltip && ( - {tooltip} + + {tooltip} + )}
    diff --git a/app/_components/session-form.tsx b/app/_components/session-form.tsx index 5bf0fd90..a49bd08f 100644 --- a/app/_components/session-form.tsx +++ b/app/_components/session-form.tsx @@ -2,9 +2,9 @@ import Button from '@/_components/button'; import DateTime from '@/_components/date-time'; -import FormBanner from '@/_components/form-banner'; import Input from '@/_components/input'; import ModuleFormSection from '@/_components/module-form-section'; +import UnsavedChangesBanner from '@/_components/unsaved-changes-banner'; import useCachedForm from '@/_hooks/use-cached-form'; import upsertSession from '@/_mutations/upsert-session'; import { GetMissionWithSessionsData } from '@/_queries/get-mission-with-sessions'; @@ -146,7 +146,7 @@ const SessionForm = ({ return ( <> startTransition(async () => { values.scheduledFor = values.scheduledFor @@ -172,16 +172,11 @@ const SessionForm = ({ return; } - localStorage.setItem('refresh', '1'); router.back(); }), )} > - - className="mt-7 border-y border-alpha-1" - form={form} - /> -
    +
    -
      +
      -
      - -
      + {form.formState.errors.root && ( -
      +
      {form.formState.errors.root.message}
      )} -
      +
      {draft && (
      + form={form} />
      - + Schedule session Scheduled sessions are not visible to clients until the diff --git a/app/_components/session-layout.tsx b/app/_components/session-layout.tsx index 6ec520ca..9f49cdac 100644 --- a/app/_components/session-layout.tsx +++ b/app/_components/session-layout.tsx @@ -88,7 +88,7 @@ const SessionLayout = async ({ return ( <> -