From 5170d883ccbd8a6732827af274adf13a68e331c8 Mon Sep 17 00:00:00 2001 From: Brandon Egger <brandonegger64@gmail.com> Date: Sun, 4 Jun 2023 22:30:07 -0500 Subject: [PATCH] add multiselector form --- src/components/ResourceTable.tsx | 36 +------- src/components/admin/resources/form.tsx | 109 ++++++++++++++++-------- src/components/forms/selectors.tsx | 61 +++++++++++++ src/components/forms/textInput.tsx | 38 +++++++++ src/prices/Icons.tsx | 35 ++++++++ 5 files changed, 211 insertions(+), 68 deletions(-) create mode 100644 src/components/forms/selectors.tsx create mode 100644 src/components/forms/textInput.tsx create mode 100644 src/prices/Icons.tsx diff --git a/src/components/ResourceTable.tsx b/src/components/ResourceTable.tsx index f6ce3a9..9d15515 100644 --- a/src/components/ResourceTable.tsx +++ b/src/components/ResourceTable.tsx @@ -1,15 +1,10 @@ import { type PlatformLink, - type PaymentType, type AuditoryResource, type Skill, type SkillLevel, type Manufacturer, } from "@prisma/client"; -import { - CurrencyDollarIcon, - ArrowPathRoundedSquareIcon, -} from "@heroicons/react/24/solid"; import { ClipboardDocumentListIcon } from "@heroicons/react/24/outline"; import Image from "next/image"; import Link from "next/link"; @@ -18,6 +13,7 @@ import { type ChangeEvent } from "react"; import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { type ParsedUrlQuery, type ParsedUrlQueryInput } from "querystring"; import { useRouter } from "next/router"; +import { PriceIcon } from "~/prices/Icons"; export const ResourceInfo = ({ resource, @@ -26,34 +22,6 @@ export const ResourceInfo = ({ resource: AuditoryResource; showMoreInfo?: boolean; }) => { - const PriceIcons = ({ type }: { type: PaymentType }) => { - switch (type) { - case "FREE": { - return ( - <div className="space-x-1 pt-2" title="Free"> - <span className="rounded-lg border border-neutral-900 bg-amber-100 px-2 py-[1px] italic text-black"> - free - </span> - </div> - ); - } - case "SUBSCRIPTION_MONTHLY": { - <div className="space-x-1" title="Monthly recurring subscription"> - <ArrowPathRoundedSquareIcon className="inline h-6 w-6" /> - <CurrencyDollarIcon className="inline h-6 w-6 text-lime-800" /> - </div>; - } - case "SUBSCRIPTION_WEEKLY": { - return ( - <div className="space-x-1" title="Weekly recurring subscription"> - <ArrowPathRoundedSquareIcon className="inline h-6 w-6" /> - <CurrencyDollarIcon className="inline h-6 w-6 text-lime-800" /> - </div> - ); - } - } - }; - const PlatformInfo = ({ platformLinks, }: { @@ -105,7 +73,7 @@ export const ResourceInfo = ({ </h2> <h1 className="text-xl font-bold">{resource.name}</h1> <PlatformInfo platformLinks={resource.platform_links} /> - <PriceIcons type={resource?.payment_options[0] ?? "FREE"} /> + <PriceIcon type={resource?.payment_options[0] ?? "FREE"} /> </div> </div> </div> diff --git a/src/components/admin/resources/form.tsx b/src/components/admin/resources/form.tsx index ba1f2f2..3ab1603 100644 --- a/src/components/admin/resources/form.tsx +++ b/src/components/admin/resources/form.tsx @@ -1,7 +1,13 @@ -import { type AuditoryResource } from "@prisma/client"; +import { PaymentType, type AuditoryResource } from "@prisma/client"; import Image from "next/image"; import { PencilSquareIcon } from "@heroicons/react/24/solid"; -import { useState } from "react"; +import { + MultiSelector, + MultiSelectorContext, + MultiSelectorOption, +} from "../../forms/selectors"; +import { InfoInputLine } from "~/components/forms/textInput"; +import { PriceIcon } from "~/prices/Icons"; /** * Renders the image selector for resource form. @@ -56,29 +62,38 @@ const ResourceLinkSubForm = ({}) => { ); }; -/** - * Single line input for the fields found to the right of the - * resource logo. - */ -const InfoInputLine = ({ - value, - placeholder, +const PaymentTypeOption = ({ + type, + label, }: { - value: string; - placeholder: string; + type: PaymentType; + label: string; }) => { - const [currentValue, setCurrentValue] = useState<string>(value); - return ( - <input - onChange={(event) => { - setCurrentValue(event.target.value); - }} - placeholder={placeholder} - value={currentValue} - type="text" - className="w-full border-b border-neutral-300 px-2" - /> + <MultiSelectorOption value={type}> + <MultiSelectorContext.Consumer> + {({ selected }) => ( + <div + className={ + (selected === type ? "bg-stone-800" : "bg-white") + + " flex flex-row space-x-2 whitespace-nowrap rounded-xl border border-neutral-400 py-[1px] pl-[1px] pr-2" + } + > + <span className="rounded-[10px] bg-white p-1"> + <PriceIcon type={type} /> + </span> + <span + className={ + (selected === type ? "text-white" : "text black") + + " my-auto inline-block whitespace-nowrap text-sm" + } + > + {label} + </span> + </div> + )} + </MultiSelectorContext.Consumer> + </MultiSelectorOption> ); }; @@ -91,41 +106,67 @@ const ResourceSummarySubForm = ({ resource?: AuditoryResource; }) => { return ( - <> - <div className="flex flex-row space-x-4 p-4"> + <div className="space-y-4 px-4"> + <div className="flex flex-row space-x-4 pt-4"> <div className="flex w-20 flex-col justify-center space-y-2 sm:w-28"> <SelectImageInput file={resource?.icon} /> </div> <div className="w-full overflow-hidden rounded-xl border border-neutral-400 bg-white drop-shadow-lg"> - <span className="text-sm"> + <span className="text-md"> <InfoInputLine placeholder="manufacturer" value={resource?.manufacturer?.name ?? ""} + hint="manufacturer" /> </span> - <InfoInputLine placeholder="name" value={resource?.name ?? ""} /> - <span className="my-2 block w-full text-center text-sm italic text-neutral-400"> + <InfoInputLine + placeholder="name" + value={resource?.name ?? ""} + hint="name" + /> + <span className="my-1 block w-full text-center text-xs italic text-neutral-400"> Edit the fields above </span> </div> </div> - <div className="mx-4 overflow-hidden rounded-xl border border-neutral-400 bg-neutral-200 text-left shadow"></div> - <div className="ml-4 mr-auto mt-4 rounded-lg border-2 border-neutral-900 bg-neutral-600"> - <span className="px-2 py-2 text-sm text-neutral-200"> - Ages {/** Age range here */} - </span> + <div> + <MultiSelector + label="Price Category" + defaultValue={ + resource?.payment_options.toString() ?? PaymentType.FREE.toString() + } + > + <PaymentTypeOption type={PaymentType.FREE} label="Free" /> + <PaymentTypeOption + type={PaymentType.SUBSCRIPTION_MONTHLY} + label="Monthly Subscription" + /> + <PaymentTypeOption + type={PaymentType.SUBSCRIPTION_WEEKLY} + label="Weekly Subscription" + /> + </MultiSelector> </div> - </> + </div> ); }; +// TODO: +// const ResourceDescriptionSubForm = ({ +// resource, +// }: { +// resource?: AuditoryResource; +// }) => { +// return <div></div>; +// }; + const ResourceForm = ({ resource }: { resource?: AuditoryResource }) => { return ( <div className="mx-auto flex max-w-2xl flex-col flex-col-reverse divide-x py-4 sm:flex-row"> <div className="my-5 mr-4 flex flex-col justify-end text-lg font-bold"> <ResourceLinkSubForm /> {/** //resource={resource} /> */} </div> - <div className="justify-left flex flex-col pb-5"> + <div className="justify-left flex max-w-lg flex-col pb-5"> <ResourceSummarySubForm resource={resource} /> </div> </div> diff --git a/src/components/forms/selectors.tsx b/src/components/forms/selectors.tsx new file mode 100644 index 0000000..e258099 --- /dev/null +++ b/src/components/forms/selectors.tsx @@ -0,0 +1,61 @@ +import { createContext, useContext, useState } from "react"; + +// Define contexts +const MultiSelectorContext = createContext({ + selected: "", + updateCallback: (_value: string) => { + return; + }, +}); + +function MultiSelector<T extends { toString: () => string }>({ + label, + defaultValue, + children, +}: { + label: string; + defaultValue: T; + children: undefined | JSX.Element | JSX.Element[]; +}) { + const [selected, setSelected] = useState<string>(defaultValue.toString()); + + return ( + <MultiSelectorContext.Provider + value={{ selected, updateCallback: setSelected }} + > + <div className="flex flex-col"> + <label className="text-md block font-semibold">{label}</label> + <span className="block text-sm italic text-neutral-400"> + Select one from below + </span> + <input readOnly type="text" className="hidden" value={selected ?? ""} /> + <div className="mt-2 flex flex-row space-x-2 overflow-x-auto"> + {children} + </div> + </div> + </MultiSelectorContext.Provider> + ); +} + +function MultiSelectorOption<T extends { toString: () => string }>({ + value, + children, +}: { + value: T; + children: undefined | JSX.Element | JSX.Element[]; +}) { + const { updateCallback } = useContext(MultiSelectorContext); + + return ( + <button + type="button" + onClick={() => { + updateCallback?.(value.toString()); + }} + > + {children} + </button> + ); +} + +export { MultiSelectorContext, MultiSelector, MultiSelectorOption }; diff --git a/src/components/forms/textInput.tsx b/src/components/forms/textInput.tsx new file mode 100644 index 0000000..21a92ac --- /dev/null +++ b/src/components/forms/textInput.tsx @@ -0,0 +1,38 @@ +import { useState } from "react"; + +/** + * Single line input for the fields found to the right of the + * resource logo. + */ +const InfoInputLine = ({ + value, + placeholder, + hint, +}: { + value: string; + placeholder: string; + hint?: string; +}) => { + const [currentValue, setCurrentValue] = useState<string>(value); + + return ( + <div className="relative"> + <input + onChange={(event) => { + setCurrentValue(event.target.value); + }} + placeholder={placeholder} + value={currentValue} + type="text" + className="w-full border-b border-neutral-300 px-2" + /> + <label className="absolute bottom-0 right-2 top-0 text-right text-sm"> + {currentValue !== "" ? ( + <span className="italic text-neutral-400">{hint}</span> + ) : undefined} + </label> + </div> + ); +}; + +export { InfoInputLine }; diff --git a/src/prices/Icons.tsx b/src/prices/Icons.tsx new file mode 100644 index 0000000..3a6f4bf --- /dev/null +++ b/src/prices/Icons.tsx @@ -0,0 +1,35 @@ +import { type PaymentType } from "@prisma/client"; +import { + CurrencyDollarIcon, + ArrowPathRoundedSquareIcon, +} from "@heroicons/react/24/solid"; + +const PriceIcon = ({ type }: { type: PaymentType }) => { + switch (type) { + case "FREE": { + return ( + <div className="space-x-1" title="Free"> + <span className="rounded-lg border border-neutral-900 bg-amber-100 px-2 py-[1px] italic text-black"> + free + </span> + </div> + ); + } + case "SUBSCRIPTION_MONTHLY": { + <div className="space-x-1" title="Monthly recurring subscription"> + <ArrowPathRoundedSquareIcon className="inline-block h-6 w-6" /> + <CurrencyDollarIcon className="inline h-6 w-6 text-lime-800" /> + </div>; + } + case "SUBSCRIPTION_WEEKLY": { + return ( + <div className="space-x-1" title="Weekly recurring subscription"> + <ArrowPathRoundedSquareIcon className="inline-block h-6 w-6" /> + <CurrencyDollarIcon className="inline h-6 w-6 text-lime-800" /> + </div> + ); + } + } +}; + +export { PriceIcon };