add prettier lint rule
This commit is contained in:
@ -1,140 +1,173 @@
|
||||
import { type NextPage } from "next/types";
|
||||
import Image from 'next/image';
|
||||
import Image from "next/image";
|
||||
|
||||
interface QuickLink {
|
||||
label: string,
|
||||
href: string,
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
const links: QuickLink[] = [
|
||||
{
|
||||
label: "Communication Sciences and Disorders",
|
||||
href: "https://csd.uiowa.edu/",
|
||||
},
|
||||
{
|
||||
label: "Wendell Johnson",
|
||||
href: "https://www.facilities.uiowa.edu/named-building/wendell-johnson-speech-and-hearing-center",
|
||||
}
|
||||
]
|
||||
{
|
||||
label: "Communication Sciences and Disorders",
|
||||
href: "https://csd.uiowa.edu/",
|
||||
},
|
||||
{
|
||||
label: "Wendell Johnson",
|
||||
href: "https://www.facilities.uiowa.edu/named-building/wendell-johnson-speech-and-hearing-center",
|
||||
},
|
||||
];
|
||||
|
||||
const QuickLink = ({label, href}: QuickLink) => {
|
||||
return (
|
||||
<a className="hover:underline space-x-2" target="_blank" href={href}>
|
||||
<Image className="inline" alt="external link" width={16} height={16} src="/open-external-link-icon.svg" />
|
||||
<span className="inline">{label}</span>
|
||||
</a>
|
||||
)
|
||||
const QuickLink = ({ label, href }: QuickLink) => {
|
||||
return (
|
||||
<a className="space-x-2 hover:underline" target="_blank" href={href}>
|
||||
<Image
|
||||
className="inline"
|
||||
alt="external link"
|
||||
width={16}
|
||||
height={16}
|
||||
src="/open-external-link-icon.svg"
|
||||
/>
|
||||
<span className="inline">{label}</span>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
interface ContactInfo {
|
||||
name: string,
|
||||
title: string,
|
||||
email?: string,
|
||||
phone?: string,
|
||||
name: string;
|
||||
title: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
const contacts: ContactInfo[] = [
|
||||
{
|
||||
name: "Olivia Adamson",
|
||||
title: "Audiology Graduate Student Clinician",
|
||||
email: "olivia-adamson@uiowa.edu",
|
||||
},
|
||||
{
|
||||
name: "Eun Kyung (Julie) Jeon",
|
||||
title: "Clinical Assistant Professor",
|
||||
email: "eunkyung-jeon@uiowa.edu",
|
||||
phone: "3194671476"
|
||||
}
|
||||
]
|
||||
{
|
||||
name: "Olivia Adamson",
|
||||
title: "Audiology Graduate Student Clinician",
|
||||
email: "olivia-adamson@uiowa.edu",
|
||||
},
|
||||
{
|
||||
name: "Eun Kyung (Julie) Jeon",
|
||||
title: "Clinical Assistant Professor",
|
||||
email: "eunkyung-jeon@uiowa.edu",
|
||||
phone: "3194671476",
|
||||
},
|
||||
];
|
||||
|
||||
const ContactInfo = ({name, title, email, phone}: ContactInfo) => {
|
||||
return (
|
||||
<section className="py-4 space-y-2">
|
||||
<h1 className="text-md">{name}</h1>
|
||||
<p className="italic text-sm text-neutral-400">{title}</p>
|
||||
{ email ?
|
||||
<section className="space-x-2">
|
||||
<Image className="inline" alt="email" width={20} height={20} src="/mail-icon-white.svg"/>
|
||||
<h2 className="text-sm inline">{email}</h2>
|
||||
</section>
|
||||
: undefined}
|
||||
{ phone ?
|
||||
<section className="space-x-2">
|
||||
<Image className="inline" alt="phone" width={20} height={20} src="/phone-call-icon.svg"/>
|
||||
<h2 className="text-sm inline">{phone}</h2>
|
||||
</section>
|
||||
: undefined}
|
||||
const ContactInfo = ({ name, title, email, phone }: ContactInfo) => {
|
||||
return (
|
||||
<section className="space-y-2 py-4">
|
||||
<h1 className="text-md">{name}</h1>
|
||||
<p className="text-sm italic text-neutral-400">{title}</p>
|
||||
{email ? (
|
||||
<section className="space-x-2">
|
||||
<Image
|
||||
className="inline"
|
||||
alt="email"
|
||||
width={20}
|
||||
height={20}
|
||||
src="/mail-icon-white.svg"
|
||||
/>
|
||||
<h2 className="inline text-sm">{email}</h2>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
) : undefined}
|
||||
{phone ? (
|
||||
<section className="space-x-2">
|
||||
<Image
|
||||
className="inline"
|
||||
alt="phone"
|
||||
width={20}
|
||||
height={20}
|
||||
src="/phone-call-icon.svg"
|
||||
/>
|
||||
<h2 className="inline text-sm">{phone}</h2>
|
||||
</section>
|
||||
) : undefined}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const FooterLabeledSection = ({title, children}: {
|
||||
title: string,
|
||||
children: JSX.Element[] | JSX.Element,
|
||||
const FooterLabeledSection = ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: JSX.Element[] | JSX.Element;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col px-2 sm:px-8">
|
||||
<h1 className="font-bold text-xl text-neutral-400">{title}</h1>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col px-2 sm:px-8">
|
||||
<h1 className="text-xl font-bold text-neutral-400">{title}</h1>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Footer: NextPage = () => {
|
||||
return (
|
||||
<div className="w-full bg-neutral-800">
|
||||
{/** yellow stripe */}
|
||||
<div className="bg-yellow-400 border-t-[1px] border-neutral-400 p-[4px]"></div>
|
||||
return (
|
||||
<div className="w-full bg-neutral-800">
|
||||
{/** yellow stripe */}
|
||||
<div className="border-t-[1px] border-neutral-400 bg-yellow-400 p-[4px]"></div>
|
||||
|
||||
{/** Main footer area */}
|
||||
<div className="mx-auto max-w-5xl p-4 flex flex-col-reverse md:flex-row justify-between">
|
||||
{/** Wendell Johnson Info */}
|
||||
<div className="flex-col mt-8 sm:mt-0">
|
||||
<Image alt="University of Iowa logo" width={128} height={64} src="/IOWA-gold-text.png" />
|
||||
<div className="px-2 text-neutral-100 space-y-8">
|
||||
<section>
|
||||
<h1 className="text-yellow-300 text-md">Communication Sciences and Disorders</h1>
|
||||
<h2 className="text-yellow-100 italic text-sm">College of Liberal Arts and Sciences</h2>
|
||||
</section>
|
||||
<section>
|
||||
<h3 className="text-sm italic">Wendell Johnson Speech and Hearing Center</h3>
|
||||
<p className="text-sm">250 Hawkins Dr</p>
|
||||
<p className="text-sm">Iowa City, IA 52242</p>
|
||||
</section>
|
||||
<section>
|
||||
<p className="text-sm text-neutral-400 italic">
|
||||
Site Designed and Built by <a target="_blank" href="https://brandonegger.com" className="hover:underline">
|
||||
Brandon Egger
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/** Header and tabs */}
|
||||
<div className="flex flex-row text-neutral-200 mx-auto md:mx-0 sm:px-4 divide-x divide-neutral-500">
|
||||
<FooterLabeledSection title="Quick Links">
|
||||
<div className="flex flex-col pt-4 space-y-2">
|
||||
{links.map((quickLink, index) => {
|
||||
return (
|
||||
<QuickLink key={index} {...quickLink}/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FooterLabeledSection>
|
||||
<FooterLabeledSection title="Contact">
|
||||
<div className="flex flex-col divide-y divide-neutral-500">
|
||||
{contacts.map((contactInfo, index) => {
|
||||
return (
|
||||
<ContactInfo key={index} {...contactInfo} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FooterLabeledSection>
|
||||
</div>
|
||||
</div>
|
||||
{/** Main footer area */}
|
||||
<div className="mx-auto flex max-w-5xl flex-col-reverse justify-between p-4 md:flex-row">
|
||||
{/** Wendell Johnson Info */}
|
||||
<div className="mt-8 flex-col sm:mt-0">
|
||||
<Image
|
||||
alt="University of Iowa logo"
|
||||
width={128}
|
||||
height={64}
|
||||
src="/IOWA-gold-text.png"
|
||||
/>
|
||||
<div className="space-y-8 px-2 text-neutral-100">
|
||||
<section>
|
||||
<h1 className="text-md text-yellow-300">
|
||||
Communication Sciences and Disorders
|
||||
</h1>
|
||||
<h2 className="text-sm italic text-yellow-100">
|
||||
College of Liberal Arts and Sciences
|
||||
</h2>
|
||||
</section>
|
||||
<section>
|
||||
<h3 className="text-sm italic">
|
||||
Wendell Johnson Speech and Hearing Center
|
||||
</h3>
|
||||
<p className="text-sm">250 Hawkins Dr</p>
|
||||
<p className="text-sm">Iowa City, IA 52242</p>
|
||||
</section>
|
||||
<section>
|
||||
<p className="text-sm italic text-neutral-400">
|
||||
Site Designed and Built by{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://brandonegger.com"
|
||||
className="hover:underline"
|
||||
>
|
||||
Brandon Egger
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/** Header and tabs */}
|
||||
<div className="mx-auto flex flex-row divide-x divide-neutral-500 text-neutral-200 sm:px-4 md:mx-0">
|
||||
<FooterLabeledSection title="Quick Links">
|
||||
<div className="flex flex-col space-y-2 pt-4">
|
||||
{links.map((quickLink, index) => {
|
||||
return <QuickLink key={index} {...quickLink} />;
|
||||
})}
|
||||
</div>
|
||||
</FooterLabeledSection>
|
||||
<FooterLabeledSection title="Contact">
|
||||
<div className="flex flex-col divide-y divide-neutral-500">
|
||||
{contacts.map((contactInfo, index) => {
|
||||
return <ContactInfo key={index} {...contactInfo} />;
|
||||
})}
|
||||
</div>
|
||||
</FooterLabeledSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
@ -1,101 +1,141 @@
|
||||
import { type NextPage } from "next";
|
||||
import Image from 'next/image';
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
interface DropdownOption {
|
||||
label: string;
|
||||
href: string;
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface NavBarLinkProps {
|
||||
href: string;
|
||||
label: string;
|
||||
dropdown?: DropdownOption[];
|
||||
href: string;
|
||||
label: string;
|
||||
dropdown?: DropdownOption[];
|
||||
}
|
||||
|
||||
const NavBarLink = ({href, label, dropdown}: NavBarLinkProps) => {
|
||||
const DropDown = ({dropdownOptions}: {dropdownOptions: DropdownOption[]}) => {
|
||||
const options = dropdownOptions.map((dropdownOption, index) => {
|
||||
return (
|
||||
<Link key={index} href={dropdownOption.href}>
|
||||
<span className="block w-full px-4 py-2 bg-gradient-to-t hover:from-neutral-500 from-neutral-900 hover:to-neutral-500 to-neutral-700 text-white">{dropdownOption.label}</span>
|
||||
</Link>
|
||||
)
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="right-0 rounded-b border-l-2 border-r-2 border-b-2 border-neutral-900 absolute w-full left-0 hidden group-hover:flex flex-col top-full">
|
||||
{options}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const NavBarLink = ({ href, label, dropdown }: NavBarLinkProps) => {
|
||||
const DropDown = ({
|
||||
dropdownOptions,
|
||||
}: {
|
||||
dropdownOptions: DropdownOption[];
|
||||
}) => {
|
||||
const options = dropdownOptions.map((dropdownOption, index) => {
|
||||
return (
|
||||
<Link key={index} href={dropdownOption.href}>
|
||||
<span className="block w-full bg-gradient-to-t from-neutral-900 to-neutral-700 px-4 py-2 text-white hover:from-neutral-500 hover:to-neutral-500">
|
||||
{dropdownOption.label}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<li className="group relative">
|
||||
<Link href={href} className={"h-14 block border-neutral-800 box-border" + (dropdown ? "" : " hover:border-b-2")}>
|
||||
<div className="h-full flex flex-row space-x-[4px]">
|
||||
<div className="inline-block my-auto">
|
||||
<span className="inline-block font-bold text-lg py-2 align-text-middle">{label}</span>
|
||||
</div>
|
||||
{dropdown ? <ChevronDownIcon className="w-4"/> : <></>}
|
||||
</div>
|
||||
</Link>
|
||||
{dropdown && dropdown.length > 0 ? <DropDown dropdownOptions={dropdown} /> : <></>}
|
||||
</li>
|
||||
<div className="absolute right-0 left-0 top-full hidden w-full flex-col rounded-b border-l-2 border-r-2 border-b-2 border-neutral-900 group-hover:flex">
|
||||
{options}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const NavBar = () => {
|
||||
const resourcesDropDown: DropdownOption[] = [
|
||||
{
|
||||
label: "search",
|
||||
href: "/resources/search",
|
||||
},
|
||||
{
|
||||
label: "view all",
|
||||
href: "/resources"
|
||||
return (
|
||||
<li className="group relative">
|
||||
<Link
|
||||
href={href}
|
||||
className={
|
||||
"box-border block h-14 border-neutral-800" +
|
||||
(dropdown ? "" : " hover:border-b-2")
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-50 border-b border-neutral-400 bg-gradient-to-b from-amber-300 to-amber-300 w-full shadow-black drop-shadow-md">
|
||||
<li className="mx-auto max-w-5xl flex flex-row sm:justify-between px-4">
|
||||
<ul id="left-nav-links" className="flex flex-row space-x-10">
|
||||
<NavBarLink href='/' label='Home'/>
|
||||
<NavBarLink dropdown={resourcesDropDown} href='/resources' label='Resources'/>
|
||||
<NavBarLink href='/about' label='About'/>
|
||||
</ul>
|
||||
|
||||
<ul id="right-nav-links" className="hidden sm:flex flex-row space-x-10">
|
||||
<li className="group relative">
|
||||
<a target="_blank" href='https://forms.gle/FD2abgwBuTaipysZ6' className="h-14 block border-neutral-800 box-border hover:border-b-2">
|
||||
<div className="h-full flex flex-row space-x-[4px]">
|
||||
<div className="inline-block my-auto">
|
||||
<span className="inline-block font-bold text-lg py-2 align-text-middle">Provide Feedback</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<NavBarLink href='/contact' label='Contact Us'/>
|
||||
</ul>
|
||||
</li>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
const Header: NextPage = () => {
|
||||
return <>
|
||||
<div id="logo-row" className="flex flex-row p-4 justify-center border-b border-yellow bg-neutral-800 drop-shadow-xl">
|
||||
<div className="shadow-md shadow-yellow-500/50 bg-yellow-100 rounded-xl p-2">
|
||||
<Image alt="Ear listening" src="/listening-ear.svg" width={64} height={64}/>
|
||||
</div>
|
||||
<div id="header-title" className="w-64 grid place-items-center">
|
||||
<h1 className="text-center text-2xl font-bold text-neutral-200">Center for Auditory Training Resources</h1>
|
||||
</div>
|
||||
>
|
||||
<div className="flex h-full flex-row space-x-[4px]">
|
||||
<div className="my-auto inline-block">
|
||||
<span className="align-text-middle inline-block py-2 text-lg font-bold">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{dropdown ? <ChevronDownIcon className="w-4" /> : <></>}
|
||||
</div>
|
||||
<NavBar/>
|
||||
</>
|
||||
</Link>
|
||||
{dropdown && dropdown.length > 0 ? (
|
||||
<DropDown dropdownOptions={dropdown} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
const NavBar = () => {
|
||||
const resourcesDropDown: DropdownOption[] = [
|
||||
{
|
||||
label: "search",
|
||||
href: "/resources/search",
|
||||
},
|
||||
{
|
||||
label: "view all",
|
||||
href: "/resources",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-50 w-full border-b border-neutral-400 bg-gradient-to-b from-amber-300 to-amber-300 shadow-black drop-shadow-md">
|
||||
<li className="mx-auto flex max-w-5xl flex-row px-4 sm:justify-between">
|
||||
<ul id="left-nav-links" className="flex flex-row space-x-10">
|
||||
<NavBarLink href="/" label="Home" />
|
||||
<NavBarLink
|
||||
dropdown={resourcesDropDown}
|
||||
href="/resources"
|
||||
label="Resources"
|
||||
/>
|
||||
<NavBarLink href="/about" label="About" />
|
||||
</ul>
|
||||
|
||||
<ul id="right-nav-links" className="hidden flex-row space-x-10 sm:flex">
|
||||
<li className="group relative">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://forms.gle/FD2abgwBuTaipysZ6"
|
||||
className="box-border block h-14 border-neutral-800 hover:border-b-2"
|
||||
>
|
||||
<div className="flex h-full flex-row space-x-[4px]">
|
||||
<div className="my-auto inline-block">
|
||||
<span className="align-text-middle inline-block py-2 text-lg font-bold">
|
||||
Provide Feedback
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<NavBarLink href="/contact" label="Contact Us" />
|
||||
</ul>
|
||||
</li>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
const Header: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id="logo-row"
|
||||
className="border-yellow flex flex-row justify-center border-b bg-neutral-800 p-4 drop-shadow-xl"
|
||||
>
|
||||
<div className="rounded-xl bg-yellow-100 p-2 shadow-md shadow-yellow-500/50">
|
||||
<Image
|
||||
alt="Ear listening"
|
||||
src="/listening-ear.svg"
|
||||
width={64}
|
||||
height={64}
|
||||
/>
|
||||
</div>
|
||||
<div id="header-title" className="grid w-64 place-items-center">
|
||||
<h1 className="text-center text-2xl font-bold text-neutral-200">
|
||||
Center for Auditory Training Resources
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<NavBar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
@ -1,297 +1,386 @@
|
||||
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';
|
||||
import { translateEnumPlatform, translateEnumSkill } from '~/utils/enumWordLut';
|
||||
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 {
|
||||
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";
|
||||
import { translateEnumPlatform, translateEnumSkill } from "~/utils/enumWordLut";
|
||||
import { type ChangeEvent } from "react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
import { type ParsedUrlQuery, type ParsedUrlQueryInput } from "querystring";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const ResourceInfo = ({resource, showMoreInfo}: {resource: AuditoryResource, showMoreInfo?: boolean}) => {
|
||||
const PriceIcons = ({type}: {type: PaymentType}) => {
|
||||
switch(type) {
|
||||
case "FREE": {
|
||||
return (
|
||||
<div className="pt-2 space-x-1" title="Free">
|
||||
<span className="bg-amber-100 italic rounded-lg border border-neutral-900 text-black px-2 py-[1px]">
|
||||
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}: {platformLinks: PlatformLink[]}) => {
|
||||
const platformsStr = platformLinks.map((platformLink) => {
|
||||
return translateEnumPlatform(platformLink.platform);
|
||||
}).join(', ');
|
||||
|
||||
export const ResourceInfo = ({
|
||||
resource,
|
||||
showMoreInfo,
|
||||
}: {
|
||||
resource: AuditoryResource;
|
||||
showMoreInfo?: boolean;
|
||||
}) => {
|
||||
const PriceIcons = ({ type }: { type: PaymentType }) => {
|
||||
switch (type) {
|
||||
case "FREE": {
|
||||
return (
|
||||
<p>{platformsStr}</p>
|
||||
)
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 space-x-4 flex flex-row">
|
||||
<div className="h-full my-auto">
|
||||
{showMoreInfo ?
|
||||
<Link href={`resources/${resource.id}`}>
|
||||
<div className="w-20 sm:w-28 flex space-y-2 flex-col justify-center">
|
||||
<Image className="bg-white w-full rounded-xl drop-shadow-lg border border-neutral-400" src={`/resource_logos/${resource.icon}`} alt={`${resource.name} logo`} width={512} height={512}/>
|
||||
<span
|
||||
className="block bg-neutral-900 hover:bg-neutral-500 border border-neutral-900 text-center py-[1px] text-white rounded-lg">
|
||||
more info
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
:
|
||||
<div className="w-20 sm:w-28 flex space-y-2 flex-col justify-center">
|
||||
<Image className="bg-white w-full rounded-xl drop-shadow-lg border border-neutral-400" src={`/resource_logos/${resource.icon}`} alt={`${resource.name} logo`} width={512} height={512}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className="grid place-items-center">
|
||||
<div className="">
|
||||
<h2 className="text-xs italic text-gray-600">{resource.manufacturer?.name}</h2>
|
||||
<h1 className="font-bold text-xl">{resource.name}</h1>
|
||||
<PlatformInfo platformLinks={resource.platform_links}/>
|
||||
<PriceIcons type={resource?.payment_options[0] ?? 'FREE'} />
|
||||
</div>
|
||||
};
|
||||
|
||||
const PlatformInfo = ({
|
||||
platformLinks,
|
||||
}: {
|
||||
platformLinks: PlatformLink[];
|
||||
}) => {
|
||||
const platformsStr = platformLinks
|
||||
.map((platformLink) => {
|
||||
return translateEnumPlatform(platformLink.platform);
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
return <p>{platformsStr}</p>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row space-x-4 p-4">
|
||||
<div className="my-auto h-full">
|
||||
{showMoreInfo ? (
|
||||
<Link href={`resources/${resource.id}`}>
|
||||
<div className="flex w-20 flex-col justify-center space-y-2 sm:w-28">
|
||||
<Image
|
||||
className="w-full rounded-xl border border-neutral-400 bg-white drop-shadow-lg"
|
||||
src={`/resource_logos/${resource.icon}`}
|
||||
alt={`${resource.name} logo`}
|
||||
width={512}
|
||||
height={512}
|
||||
/>
|
||||
<span className="block rounded-lg border border-neutral-900 bg-neutral-900 py-[1px] text-center text-white hover:bg-neutral-500">
|
||||
more info
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex w-20 flex-col justify-center space-y-2 sm:w-28">
|
||||
<Image
|
||||
className="w-full rounded-xl border border-neutral-400 bg-white drop-shadow-lg"
|
||||
src={`/resource_logos/${resource.icon}`}
|
||||
alt={`${resource.name} logo`}
|
||||
width={512}
|
||||
height={512}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid place-items-center">
|
||||
<div className="">
|
||||
<h2 className="text-xs italic text-gray-600">
|
||||
{resource.manufacturer?.name}
|
||||
</h2>
|
||||
<h1 className="text-xl font-bold">{resource.name}</h1>
|
||||
<PlatformInfo platformLinks={resource.platform_links} />
|
||||
<PriceIcons type={resource?.payment_options[0] ?? "FREE"} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ResourceDescription = ({manufacturer, description}: {manufacturer: null | Manufacturer, description: string}) => {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{ manufacturer?.required ?
|
||||
<div className="bg-neutral-600 border-t-[4px] border-neutral-700 p-2">
|
||||
<h3 className="text-sm font-bold text-neutral-100">IMPORTANT</h3>
|
||||
<p className="text-sm text-neutral-300">
|
||||
This resource requires the patient to have a {manufacturer.name} device
|
||||
</p>
|
||||
</div>
|
||||
: undefined}
|
||||
<div className="p-2">
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
export const ResourceDescription = ({
|
||||
manufacturer,
|
||||
description,
|
||||
}: {
|
||||
manufacturer: null | Manufacturer;
|
||||
description: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{manufacturer?.required ? (
|
||||
<div className="border-t-[4px] border-neutral-700 bg-neutral-600 p-2">
|
||||
<h3 className="text-sm font-bold text-neutral-100">IMPORTANT</h3>
|
||||
<p className="text-sm text-neutral-300">
|
||||
This resource requires the patient to have a {manufacturer.name}{" "}
|
||||
device
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
) : undefined}
|
||||
<div className="p-2">
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ResourceEntry = ({resource}: {resource: AuditoryResource}) => {
|
||||
const ResourceSkills = ({skills, skillLevels}: {skills: Skill[], skillLevels: SkillLevel[]}) => {
|
||||
const SkillRanking = ({skillLevels}: {skillLevels: SkillLevel[]}) => {
|
||||
return (
|
||||
<div className='flex flex-row space-x-2 overflow-x-auto'>
|
||||
{skillLevels.includes('BEGINNER') ?
|
||||
<div className="rounded-lg px-[3px] border-green-600 border-2 bg-green-300">
|
||||
<h2 className="text-neutral-900 italic text-sm text-right">Beginner</h2>
|
||||
</div> : undefined
|
||||
}
|
||||
{skillLevels.includes('INTERMEDIATE') ?
|
||||
<div className="rounded-lg px-[3px] border-orange-600 border-2 bg-orange-300">
|
||||
<h2 className="text-neutral-900 text-sm italic text-right">Intermediate</h2>
|
||||
</div> : undefined
|
||||
}
|
||||
{skillLevels.includes('ADVANCED') ?
|
||||
<div className="rounded-lg px-[3px] border-red-600 border-2 bg-red-300">
|
||||
<h2 className="text-neutral-900 text-sm italic text-right">Advanced</h2>
|
||||
</div> : undefined
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Skill = ({label}: {label:string}) => {
|
||||
return (
|
||||
<li className="space-x-2 flex flex-row px-2 py-[1px]">
|
||||
<ClipboardDocumentListIcon className="w-4" />
|
||||
<div className="inline">
|
||||
<h3>{label}</h3>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const skillsComponents = skills.map((skill, index) => {
|
||||
return <Skill key={index} label={translateEnumSkill(skill)}/>
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="m-2 flex space-y-4 flex-col">
|
||||
{ skillsComponents.length > 0 ?
|
||||
<div className='rounded-lg bg-gray-100 drop-shadow border border-neutral-900'>
|
||||
<ul className="divide-y-2">
|
||||
{skillsComponents}
|
||||
</ul>
|
||||
</div> : <></>
|
||||
}
|
||||
<SkillRanking skillLevels={skillLevels} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="divide-x-[1px] divide-slate-400">
|
||||
<td className="max-w-xs">
|
||||
<ResourceInfo showMoreInfo resource={resource} />
|
||||
</td>
|
||||
<td className="w-1/4 align-top">
|
||||
<ResourceSkills skills={resource.skills} skillLevels={resource.skill_levels} />
|
||||
</td>
|
||||
<td className="align-top hidden md:table-cell">
|
||||
<ResourceDescription manufacturer={resource.manufacturer} description={resource.description} />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
interface PagesNavigationProps {
|
||||
query?: ParsedUrlQuery;
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
resultsPerPage: number;
|
||||
}
|
||||
|
||||
const PagesNavigation = ({query, currentPage, pageCount, resultsPerPage}: PagesNavigationProps) => {
|
||||
const router = useRouter();
|
||||
const PageButton = ({number}: {number: number}) => {
|
||||
const redirectQueryData: ParsedUrlQueryInput = {...query};
|
||||
redirectQueryData.page = number;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link className={"block py px-[9px] m-1 rounded " + (currentPage !== number ? "hover:bg-neutral-400 hover:text-white" : "bg-neutral-800 text-white")}
|
||||
href={{ pathname: `/resources`, query: {...redirectQueryData} }}>
|
||||
<span className={"text-lg text-center"}>{number}</span>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const pages = Array.from(Array(pageCount).keys()).map((pageNumber) => {
|
||||
const ResourceEntry = ({ resource }: { resource: AuditoryResource }) => {
|
||||
const ResourceSkills = ({
|
||||
skills,
|
||||
skillLevels,
|
||||
}: {
|
||||
skills: Skill[];
|
||||
skillLevels: SkillLevel[];
|
||||
}) => {
|
||||
const SkillRanking = ({ skillLevels }: { skillLevels: SkillLevel[] }) => {
|
||||
return (
|
||||
<PageButton key={pageNumber} number={pageNumber+1} />
|
||||
)
|
||||
<div className="flex flex-row space-x-2 overflow-x-auto">
|
||||
{skillLevels.includes("BEGINNER") ? (
|
||||
<div className="rounded-lg border-2 border-green-600 bg-green-300 px-[3px]">
|
||||
<h2 className="text-right text-sm italic text-neutral-900">
|
||||
Beginner
|
||||
</h2>
|
||||
</div>
|
||||
) : undefined}
|
||||
{skillLevels.includes("INTERMEDIATE") ? (
|
||||
<div className="rounded-lg border-2 border-orange-600 bg-orange-300 px-[3px]">
|
||||
<h2 className="text-right text-sm italic text-neutral-900">
|
||||
Intermediate
|
||||
</h2>
|
||||
</div>
|
||||
) : undefined}
|
||||
{skillLevels.includes("ADVANCED") ? (
|
||||
<div className="rounded-lg border-2 border-red-600 bg-red-300 px-[3px]">
|
||||
<h2 className="text-right text-sm italic text-neutral-900">
|
||||
Advanced
|
||||
</h2>
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Skill = ({ label }: { label: string }) => {
|
||||
return (
|
||||
<li className="flex flex-row space-x-2 px-2 py-[1px]">
|
||||
<ClipboardDocumentListIcon className="w-4" />
|
||||
<div className="inline">
|
||||
<h3>{label}</h3>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const skillsComponents = skills.map((skill, index) => {
|
||||
return <Skill key={index} label={translateEnumSkill(skill)} />;
|
||||
});
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
if (!query) {
|
||||
router.push({
|
||||
pathname: '/resources',
|
||||
query: {
|
||||
perPage: event.target.value,
|
||||
}
|
||||
}).catch((reason) => {
|
||||
console.error(reason);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
query['perPage'] = event.target.value;
|
||||
router.push({
|
||||
pathname: '/resources',
|
||||
query: {
|
||||
...query,
|
||||
}
|
||||
}).catch((reason) => {
|
||||
console.error(reason);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between pl-2 pr-4 py-2 bg-amber-100">
|
||||
<div className="flex flex-row w-64 space-x-2">
|
||||
<div className="relative inline-flex">
|
||||
<select
|
||||
className="block appearance-none w-full bg-white border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline"
|
||||
value={resultsPerPage}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value={5}>5</option>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
</select>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||
<ChevronDownIcon className="h-4 w-4"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="m-auto">
|
||||
<h1 className="text-md"> Results Per Page</h1>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="max-w-[10rem] sm:max-w-none overflow-x-auto my-auto flex flex-row bg-white rounded border-gray-400 hover:border-gray-500 border shadow">
|
||||
{pages}
|
||||
</ul>
|
||||
<div className="m-2 flex flex-col space-y-4">
|
||||
{skillsComponents.length > 0 ? (
|
||||
<div className="rounded-lg border border-neutral-900 bg-gray-100 drop-shadow">
|
||||
<ul className="divide-y-2">{skillsComponents}</ul>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<SkillRanking skillLevels={skillLevels} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const ResourceTable = ({resources, resourcesPerPage, currentPage, totalPages, query}: {
|
||||
resources: AuditoryResource[],
|
||||
resourcesPerPage: number,
|
||||
currentPage: number,
|
||||
totalPages: number,
|
||||
query: ParsedUrlQuery
|
||||
return (
|
||||
<tr className="divide-x-[1px] divide-slate-400">
|
||||
<td className="max-w-xs">
|
||||
<ResourceInfo showMoreInfo resource={resource} />
|
||||
</td>
|
||||
<td className="w-1/4 align-top">
|
||||
<ResourceSkills
|
||||
skills={resource.skills}
|
||||
skillLevels={resource.skill_levels}
|
||||
/>
|
||||
</td>
|
||||
<td className="hidden align-top md:table-cell">
|
||||
<ResourceDescription
|
||||
manufacturer={resource.manufacturer}
|
||||
description={resource.description}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
interface PagesNavigationProps {
|
||||
query?: ParsedUrlQuery;
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
resultsPerPage: number;
|
||||
}
|
||||
|
||||
const PagesNavigation = ({
|
||||
query,
|
||||
currentPage,
|
||||
pageCount,
|
||||
resultsPerPage,
|
||||
}: PagesNavigationProps) => {
|
||||
const router = useRouter();
|
||||
const PageButton = ({ number }: { number: number }) => {
|
||||
const redirectQueryData: ParsedUrlQueryInput = { ...query };
|
||||
redirectQueryData.page = number;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
className={
|
||||
"py m-1 block rounded px-[9px] " +
|
||||
(currentPage !== number
|
||||
? "hover:bg-neutral-400 hover:text-white"
|
||||
: "bg-neutral-800 text-white")
|
||||
}
|
||||
href={{ pathname: `/resources`, query: { ...redirectQueryData } }}
|
||||
>
|
||||
<span className={"text-center text-lg"}>{number}</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const pages = Array.from(Array(pageCount).keys()).map((pageNumber) => {
|
||||
return <PageButton key={pageNumber} number={pageNumber + 1} />;
|
||||
});
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
if (!query) {
|
||||
router
|
||||
.push({
|
||||
pathname: "/resources",
|
||||
query: {
|
||||
perPage: event.target.value,
|
||||
},
|
||||
})
|
||||
.catch((reason) => {
|
||||
console.error(reason);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
query["perPage"] = event.target.value;
|
||||
router
|
||||
.push({
|
||||
pathname: "/resources",
|
||||
query: {
|
||||
...query,
|
||||
},
|
||||
})
|
||||
.catch((reason) => {
|
||||
console.error(reason);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between bg-amber-100 py-2 pl-2 pr-4">
|
||||
<div className="flex w-64 flex-row space-x-2">
|
||||
<div className="relative inline-flex">
|
||||
<select
|
||||
className="focus:shadow-outline block w-full appearance-none rounded border border-gray-400 bg-white px-4 py-2 pr-8 leading-tight shadow hover:border-gray-500 focus:outline-none"
|
||||
value={resultsPerPage}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value={5}>5</option>
|
||||
<option value={10}>10</option>
|
||||
<option value={20}>20</option>
|
||||
</select>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||
<ChevronDownIcon className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="m-auto">
|
||||
<h1 className="text-md"> Results Per Page</h1>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="my-auto flex max-w-[10rem] flex-row overflow-x-auto rounded border border-gray-400 bg-white shadow hover:border-gray-500 sm:max-w-none">
|
||||
{pages}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ResourceTable = ({
|
||||
resources,
|
||||
resourcesPerPage,
|
||||
currentPage,
|
||||
totalPages,
|
||||
query,
|
||||
}: {
|
||||
resources: AuditoryResource[];
|
||||
resourcesPerPage: number;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
query: ParsedUrlQuery;
|
||||
}) => {
|
||||
const resourceElements = resources.map((resource, index) => {
|
||||
return (<ResourceEntry key={index} resource={resource} />);
|
||||
const resourceElements =
|
||||
resources.map((resource, index) => {
|
||||
return <ResourceEntry key={index} resource={resource} />;
|
||||
}) ?? [];
|
||||
|
||||
return(
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mx-auto rounded-xl overflow-hidden border border-neutral-400 drop-shadow-md overflow-hidden">
|
||||
<PagesNavigation query={query} resultsPerPage={resourcesPerPage} currentPage={currentPage} pageCount={totalPages} />
|
||||
<table className="w-full table-fixed bg-neutral-200 border-b border-neutral-400">
|
||||
<thead className="bg-gradient-to-t from-neutral-900 to-neutral-700 drop-shadow-md">
|
||||
<tr>
|
||||
<th className="w-1/3 max-w-xs">
|
||||
<span className="text-gray-300 block px-4 py-2 text-left">
|
||||
Resource
|
||||
</span>
|
||||
</th>
|
||||
<th className="w-1/4 max-w-xs">
|
||||
<span className="text-gray-300 block px-4 py-2 text-left">
|
||||
Skills
|
||||
</span>
|
||||
</th>
|
||||
<th className="hidden md:table-cell">
|
||||
<span className="text-gray-300 block px-4 py-2 text-left">
|
||||
Description
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y-[1px] divide-slate-400 overflow-y-scroll">
|
||||
{resourceElements}
|
||||
</tbody>
|
||||
</table>
|
||||
{(resources && resources.length > 4) ?
|
||||
<PagesNavigation query={query} resultsPerPage={resourcesPerPage} currentPage={currentPage} pageCount={totalPages} />
|
||||
: undefined}
|
||||
</div>
|
||||
<div className="mx-auto overflow-hidden overflow-hidden rounded-xl border border-neutral-400 drop-shadow-md">
|
||||
<PagesNavigation
|
||||
query={query}
|
||||
resultsPerPage={resourcesPerPage}
|
||||
currentPage={currentPage}
|
||||
pageCount={totalPages}
|
||||
/>
|
||||
<table className="w-full table-fixed border-b border-neutral-400 bg-neutral-200">
|
||||
<thead className="bg-gradient-to-t from-neutral-900 to-neutral-700 drop-shadow-md">
|
||||
<tr>
|
||||
<th className="w-1/3 max-w-xs">
|
||||
<span className="block px-4 py-2 text-left text-gray-300">
|
||||
Resource
|
||||
</span>
|
||||
</th>
|
||||
<th className="w-1/4 max-w-xs">
|
||||
<span className="block px-4 py-2 text-left text-gray-300">
|
||||
Skills
|
||||
</span>
|
||||
</th>
|
||||
<th className="hidden md:table-cell">
|
||||
<span className="block px-4 py-2 text-left text-gray-300">
|
||||
Description
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y-[1px] divide-slate-400 overflow-y-scroll">
|
||||
{resourceElements}
|
||||
</tbody>
|
||||
</table>
|
||||
{resources && resources.length > 4 ? (
|
||||
<PagesNavigation
|
||||
query={query}
|
||||
resultsPerPage={resourcesPerPage}
|
||||
currentPage={currentPage}
|
||||
pageCount={totalPages}
|
||||
/>
|
||||
) : undefined}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceTable;
|
||||
|
@ -1,274 +1,383 @@
|
||||
import { type PaymentType, type Platform, type Skill, type SkillLevel } from "@prisma/client"
|
||||
import {
|
||||
type PaymentType,
|
||||
type Platform,
|
||||
type Skill,
|
||||
type SkillLevel,
|
||||
} from "@prisma/client";
|
||||
import { type Dispatch, type SetStateAction, useState, useEffect } from "react";
|
||||
|
||||
export type QuestionTypes = Platform | Skill | SkillLevel | PaymentType | string;
|
||||
export type QuestionTypes =
|
||||
| Platform
|
||||
| Skill
|
||||
| SkillLevel
|
||||
| PaymentType
|
||||
| string;
|
||||
|
||||
export interface Option<T> {
|
||||
label: string,
|
||||
value: T,
|
||||
label: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export interface Question<T> {
|
||||
for: string,
|
||||
header: string,
|
||||
question: string,
|
||||
maxSelect?: number,
|
||||
optional: true,
|
||||
options: Option<T>[]
|
||||
for: string;
|
||||
header: string;
|
||||
question: string;
|
||||
maxSelect?: number;
|
||||
optional: true;
|
||||
options: Option<T>[];
|
||||
}
|
||||
|
||||
const GreetingPage = ({updatePage}: {
|
||||
updatePage: (pageNumber: number) => void,
|
||||
const GreetingPage = ({
|
||||
updatePage,
|
||||
}: {
|
||||
updatePage: (pageNumber: number) => void;
|
||||
}) => {
|
||||
const getStartedClick = () => {
|
||||
updatePage(1);
|
||||
}
|
||||
const getStartedClick = () => {
|
||||
updatePage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col text-center">
|
||||
<h1 className="text-center text-xl font-extrabold mb-8 max-w-sm">Welcome to the auditory training resource search tool!</h1>
|
||||
<p className="mx-auto text-center text-neutral-500 italic max-w-sm">We will ask a few questions about the patient and then recommend the best auditory training resources based on your answers!</p>
|
||||
<button onClick={getStartedClick} className="bottom-0 mt-8 py-2 px-4 bg-yellow-100 mx-auto rounded-md border border-neutral-900 ease-out duration-200 shadow-lg hover:shadow-md hover:bg-yellow-300">Get Started!</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col text-center">
|
||||
<h1 className="mb-8 max-w-sm text-center text-xl font-extrabold">
|
||||
Welcome to the auditory training resource search tool!
|
||||
</h1>
|
||||
<p className="mx-auto max-w-sm text-center italic text-neutral-500">
|
||||
We will ask a few questions about the patient and then recommend the
|
||||
best auditory training resources based on your answers!
|
||||
</p>
|
||||
<button
|
||||
onClick={getStartedClick}
|
||||
className="bottom-0 mx-auto mt-8 rounded-md border border-neutral-900 bg-yellow-100 py-2 px-4 shadow-lg duration-200 ease-out hover:bg-yellow-300 hover:shadow-md"
|
||||
>
|
||||
Get Started!
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Single question component for a guided search
|
||||
*/
|
||||
const QuestionPage = ({isLastPage, page, question, updatePage, formData, updateFormData, dontCareData, setDontCareData}: {
|
||||
isLastPage: boolean,
|
||||
page: number,
|
||||
question: Question<QuestionTypes>,
|
||||
updatePage: (pageNumber: number) => void,
|
||||
formData: Record<string, QuestionTypes[]>,
|
||||
updateFormData: Dispatch<SetStateAction<Record<string, QuestionTypes[]>>>,
|
||||
dontCareData: Record<string, boolean>,
|
||||
setDontCareData: Dispatch<SetStateAction<Record<string, boolean>>>,
|
||||
|
||||
const QuestionPage = ({
|
||||
isLastPage,
|
||||
page,
|
||||
question,
|
||||
updatePage,
|
||||
formData,
|
||||
updateFormData,
|
||||
dontCareData,
|
||||
setDontCareData,
|
||||
}: {
|
||||
isLastPage: boolean;
|
||||
page: number;
|
||||
question: Question<QuestionTypes>;
|
||||
updatePage: (pageNumber: number) => void;
|
||||
formData: Record<string, QuestionTypes[]>;
|
||||
updateFormData: Dispatch<SetStateAction<Record<string, QuestionTypes[]>>>;
|
||||
dontCareData: Record<string, boolean>;
|
||||
setDontCareData: Dispatch<SetStateAction<Record<string, boolean>>>;
|
||||
}) => {
|
||||
const dontCare = dontCareData[question.for] ?? false;
|
||||
const dontCare = dontCareData[question.for] ?? false;
|
||||
|
||||
const OptionToggle = ({option}: {option: Option<QuestionTypes>}) => {
|
||||
const selected = formData[question.for]?.includes(option.value) ?? false;
|
||||
|
||||
const handleToggle = () => {
|
||||
const newFormData = {
|
||||
...formData
|
||||
};
|
||||
const OptionToggle = ({ option }: { option: Option<QuestionTypes> }) => {
|
||||
const selected = formData[question.for]?.includes(option.value) ?? false;
|
||||
|
||||
if (!newFormData[question.for]) {
|
||||
newFormData[question.for] = [option.value];
|
||||
} else if (newFormData[question.for]?.includes(option.value)) {
|
||||
newFormData[question.for] = newFormData[question.for]?.filter(function(item) {
|
||||
return item !== option.value
|
||||
}) ?? [];
|
||||
} else {
|
||||
newFormData[question.for] = [...newFormData[question.for] ?? [], option.value];
|
||||
}
|
||||
const handleToggle = () => {
|
||||
const newFormData = {
|
||||
...formData,
|
||||
};
|
||||
|
||||
updateFormData(newFormData);
|
||||
}
|
||||
if (!newFormData[question.for]) {
|
||||
newFormData[question.for] = [option.value];
|
||||
} else if (newFormData[question.for]?.includes(option.value)) {
|
||||
newFormData[question.for] =
|
||||
newFormData[question.for]?.filter(function (item) {
|
||||
return item !== option.value;
|
||||
}) ?? [];
|
||||
} else {
|
||||
newFormData[question.for] = [
|
||||
...(newFormData[question.for] ?? []),
|
||||
option.value,
|
||||
];
|
||||
}
|
||||
|
||||
if (dontCare) {
|
||||
return (
|
||||
<button disabled type="button" onClick={handleToggle} className={"line-through mx-auto w-64 py-2 shadow rounded-lg border border-neutral-400 " + (selected ? "bg-amber-200" : "bg-white")}>
|
||||
{option.label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" onClick={handleToggle} className={"mx-auto w-64 py-2 shadow rounded-lg border border-neutral-400 " + (selected ? "bg-amber-200" : "bg-white")}>
|
||||
{option.label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!formData[question.for]) {
|
||||
const newFormData = {...formData};
|
||||
newFormData[question.for] = [];
|
||||
|
||||
updateFormData(newFormData);
|
||||
}
|
||||
});
|
||||
|
||||
const dontCareToggle = () => {
|
||||
if (formData[question.for]) {
|
||||
const newFormData = {
|
||||
...formData
|
||||
};
|
||||
|
||||
newFormData[question.for] = [];
|
||||
updateFormData(newFormData);
|
||||
}
|
||||
|
||||
const newDontCareData = {
|
||||
...dontCareData
|
||||
}
|
||||
|
||||
newDontCareData[question.for] = !dontCare;
|
||||
setDontCareData(newDontCareData);
|
||||
}
|
||||
|
||||
const nextClick = () => {
|
||||
updatePage(page + 1);
|
||||
}
|
||||
|
||||
const backClick = () => {
|
||||
updatePage(page - 1);
|
||||
}
|
||||
|
||||
const AdvanceButtons = () => {
|
||||
return (
|
||||
<section className="">
|
||||
{!isLastPage ?
|
||||
<div className="space-x-4">
|
||||
<button onClick={backClick} className="inline mx-auto bottom-0 py-2 px-4 bg-yellow-100 mx-auto rounded-md border border-neutral-900 ease-out duration-200 shadow-lg hover:shadow-md hover:bg-yellow-300">back</button>
|
||||
<button onClick={nextClick} className="inline mx-auto bottom-0 py-2 px-4 bg-yellow-100 mx-auto rounded-md border border-neutral-900 ease-out duration-200 shadow-lg hover:shadow-md hover:bg-yellow-300">next</button>
|
||||
</div>
|
||||
:
|
||||
<div className="flex flex-col space-y-2 mt-4">
|
||||
<button onClick={backClick} className="mx-auto bottom-0 py-2 px-4 bg-yellow-100 mx-auto rounded-md border border-neutral-900 ease-out duration-200 shadow-lg hover:shadow-md hover:bg-yellow-300">back</button>
|
||||
<button form="search-form" type="submit" className="mx-auto bottom-0 py-2 px-4 bg-yellow-100 mx-auto rounded-md border border-neutral-900 ease-out duration-200 shadow-lg hover:shadow-md hover:bg-yellow-300">submit</button>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
)
|
||||
updateFormData(newFormData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col justify-between text-center">
|
||||
<section className="mt-4">
|
||||
<h2 className="text-neutral-400 italic text-xl">{question.header}</h2>
|
||||
<h1 className="text-neutral-900 font-bold text-xl">{question.question}</h1>
|
||||
<h3 className="text-neutral-600 text-sm">Select all that apply from below</h3>
|
||||
</section>
|
||||
if (dontCare) {
|
||||
return (
|
||||
<button
|
||||
disabled
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
className={
|
||||
"mx-auto w-64 rounded-lg border border-neutral-400 py-2 line-through shadow " +
|
||||
(selected ? "bg-amber-200" : "bg-white")
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
<section className="flex flex-col space-y-1 justify-center">
|
||||
{question.options.map((option, index) => {
|
||||
return (
|
||||
<OptionToggle key={index} option={option} />
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
{question.optional ?
|
||||
<button type="button" onClick={dontCareToggle} className={"mx-auto w-64 py-2 shadow rounded-lg border border-neutral-400 " + (dontCare ? "bg-amber-200" : "bg-white")}>
|
||||
No Preference
|
||||
</button>
|
||||
: undefined}
|
||||
<div className="mb-4">
|
||||
<AdvanceButtons />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggle}
|
||||
className={
|
||||
"mx-auto w-64 rounded-lg border border-neutral-400 py-2 shadow " +
|
||||
(selected ? "bg-amber-200" : "bg-white")
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!formData[question.for]) {
|
||||
const newFormData = { ...formData };
|
||||
newFormData[question.for] = [];
|
||||
|
||||
updateFormData(newFormData);
|
||||
}
|
||||
});
|
||||
|
||||
const dontCareToggle = () => {
|
||||
if (formData[question.for]) {
|
||||
const newFormData = {
|
||||
...formData,
|
||||
};
|
||||
|
||||
newFormData[question.for] = [];
|
||||
updateFormData(newFormData);
|
||||
}
|
||||
|
||||
const newDontCareData = {
|
||||
...dontCareData,
|
||||
};
|
||||
|
||||
newDontCareData[question.for] = !dontCare;
|
||||
setDontCareData(newDontCareData);
|
||||
};
|
||||
|
||||
const nextClick = () => {
|
||||
updatePage(page + 1);
|
||||
};
|
||||
|
||||
const backClick = () => {
|
||||
updatePage(page - 1);
|
||||
};
|
||||
|
||||
const AdvanceButtons = () => {
|
||||
return (
|
||||
<section className="">
|
||||
{!isLastPage ? (
|
||||
<div className="space-x-4">
|
||||
<button
|
||||
onClick={backClick}
|
||||
className="bottom-0 mx-auto mx-auto inline rounded-md border border-neutral-900 bg-yellow-100 py-2 px-4 shadow-lg duration-200 ease-out hover:bg-yellow-300 hover:shadow-md"
|
||||
>
|
||||
back
|
||||
</button>
|
||||
<button
|
||||
onClick={nextClick}
|
||||
className="bottom-0 mx-auto mx-auto inline rounded-md border border-neutral-900 bg-yellow-100 py-2 px-4 shadow-lg duration-200 ease-out hover:bg-yellow-300 hover:shadow-md"
|
||||
>
|
||||
next
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 flex flex-col space-y-2">
|
||||
<button
|
||||
onClick={backClick}
|
||||
className="bottom-0 mx-auto mx-auto rounded-md border border-neutral-900 bg-yellow-100 py-2 px-4 shadow-lg duration-200 ease-out hover:bg-yellow-300 hover:shadow-md"
|
||||
>
|
||||
back
|
||||
</button>
|
||||
<button
|
||||
form="search-form"
|
||||
type="submit"
|
||||
className="bottom-0 mx-auto mx-auto rounded-md border border-neutral-900 bg-yellow-100 py-2 px-4 shadow-lg duration-200 ease-out hover:bg-yellow-300 hover:shadow-md"
|
||||
>
|
||||
submit
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col justify-between text-center">
|
||||
<section className="mt-4">
|
||||
<h2 className="text-xl italic text-neutral-400">{question.header}</h2>
|
||||
<h1 className="text-xl font-bold text-neutral-900">
|
||||
{question.question}
|
||||
</h1>
|
||||
<h3 className="text-sm text-neutral-600">
|
||||
Select all that apply from below
|
||||
</h3>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col justify-center space-y-1">
|
||||
{question.options.map((option, index) => {
|
||||
return <OptionToggle key={index} option={option} />;
|
||||
})}
|
||||
</section>
|
||||
{question.optional ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={dontCareToggle}
|
||||
className={
|
||||
"mx-auto w-64 rounded-lg border border-neutral-400 py-2 shadow " +
|
||||
(dontCare ? "bg-amber-200" : "bg-white")
|
||||
}
|
||||
>
|
||||
No Preference
|
||||
</button>
|
||||
) : undefined}
|
||||
<div className="mb-4">
|
||||
<AdvanceButtons />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for last and current page to enable the transition animation
|
||||
*/
|
||||
const PageTransition = ({backwards, lastPage, currentPage}: {
|
||||
backwards: boolean,
|
||||
lastPage: JSX.Element | null,
|
||||
currentPage: JSX.Element,
|
||||
const PageTransition = ({
|
||||
backwards,
|
||||
lastPage,
|
||||
currentPage,
|
||||
}: {
|
||||
backwards: boolean;
|
||||
lastPage: JSX.Element | null;
|
||||
currentPage: JSX.Element;
|
||||
}) => {
|
||||
return (
|
||||
<div className={"h-[500px] w-[200%] flex " + (backwards ? "flex-row animate-slide_search_page_backwards" : "flex-row-reverse translate-x-[-50%] animate-slide_search_page")}>
|
||||
<div className="relative w-1/2 h-full grid place-items-center">
|
||||
{currentPage}
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"flex h-[500px] w-[200%] " +
|
||||
(backwards
|
||||
? "animate-slide_search_page_backwards flex-row"
|
||||
: "translate-x-[-50%] animate-slide_search_page flex-row-reverse")
|
||||
}
|
||||
>
|
||||
<div className="relative grid h-full w-1/2 place-items-center">
|
||||
{currentPage}
|
||||
</div>
|
||||
|
||||
{/** last page */}
|
||||
<div className="relative w-1/2 h-full grid place-items-center">
|
||||
{lastPage}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
{/** last page */}
|
||||
<div className="relative grid h-full w-1/2 place-items-center">
|
||||
{lastPage}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Main guided search component.
|
||||
* Page 0 = greeting page.
|
||||
*/
|
||||
const GuidedSearch = ({questions}: {
|
||||
questions: Question<QuestionTypes>[],
|
||||
const GuidedSearch = ({
|
||||
questions,
|
||||
}: {
|
||||
questions: Question<QuestionTypes>[];
|
||||
}) => {
|
||||
const [page, setPage] = useState<number>(0);
|
||||
const [formData, setFormData] = useState<(Record<string, QuestionTypes[]>)>({});
|
||||
const [dontCareData, setDoneCareData] = useState<(Record<string, boolean>)>({});
|
||||
const [previousPage, setPreviousPage] = useState<number | undefined>(undefined);
|
||||
const [page, setPage] = useState<number>(0);
|
||||
const [formData, setFormData] = useState<Record<string, QuestionTypes[]>>({});
|
||||
const [dontCareData, setDoneCareData] = useState<Record<string, boolean>>({});
|
||||
const [previousPage, setPreviousPage] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const updatePage = (pageNumber: number) => {
|
||||
setPreviousPage(page);
|
||||
setPage(pageNumber);
|
||||
};
|
||||
const updatePage = (pageNumber: number) => {
|
||||
setPreviousPage(page);
|
||||
setPage(pageNumber);
|
||||
};
|
||||
|
||||
const SearchPage = ({pageNumber}: {
|
||||
pageNumber?: number,
|
||||
}) => {
|
||||
if (pageNumber === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pageNumber === 0) {
|
||||
return (
|
||||
<GreetingPage updatePage={updatePage} />
|
||||
);
|
||||
}
|
||||
|
||||
const question = questions[pageNumber-1];
|
||||
if (!question) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isLastPage = pageNumber === questions.length
|
||||
|
||||
return (
|
||||
<QuestionPage dontCareData={dontCareData} setDontCareData={setDoneCareData} isLastPage={isLastPage} page={page} formData={formData} updateFormData={setFormData} updatePage={updatePage} question={question} />
|
||||
);
|
||||
const SearchPage = ({ pageNumber }: { pageNumber?: number }) => {
|
||||
if (pageNumber === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the hidden html form selectors
|
||||
*/
|
||||
const HTMLQuestion = ({question}: {question: Question<QuestionTypes>}) => {
|
||||
return (
|
||||
<select className="hidden" name={question.for} multiple>
|
||||
{question.options.map((option, index) => {
|
||||
return (
|
||||
<option key={index} selected={formData[question.for]?.includes(option.value)} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</option>
|
||||
);
|
||||
})
|
||||
}
|
||||
</select>
|
||||
);
|
||||
if (pageNumber === 0) {
|
||||
return <GreetingPage updatePage={updatePage} />;
|
||||
}
|
||||
|
||||
const lastPage = <SearchPage pageNumber={previousPage} />;
|
||||
const currentPage = <SearchPage pageNumber={page} />;
|
||||
const backwards = (previousPage ?? -1) >= page;
|
||||
const question = questions[pageNumber - 1];
|
||||
if (!question) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isLastPage = pageNumber === questions.length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="px-4 py-2 bg-gradient-to-t from-neutral-900 to-neutral-700 mx-auto overflow-hidden">
|
||||
<h1 className="text-gray-300 font-bold">Search</h1>
|
||||
</div>
|
||||
<QuestionPage
|
||||
dontCareData={dontCareData}
|
||||
setDontCareData={setDoneCareData}
|
||||
isLastPage={isLastPage}
|
||||
page={page}
|
||||
formData={formData}
|
||||
updateFormData={setFormData}
|
||||
updatePage={updatePage}
|
||||
question={question}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
<PageTransition backwards={backwards} key={page} lastPage={lastPage} currentPage={currentPage}/>
|
||||
/**
|
||||
* Renders the hidden html form selectors
|
||||
*/
|
||||
const HTMLQuestion = ({
|
||||
question,
|
||||
}: {
|
||||
question: Question<QuestionTypes>;
|
||||
}) => {
|
||||
return (
|
||||
<select className="hidden" name={question.for} multiple>
|
||||
{question.options.map((option, index) => {
|
||||
return (
|
||||
<option
|
||||
key={index}
|
||||
selected={formData[question.for]?.includes(option.value)}
|
||||
value={option.value.toString()}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
{/** Hidden html */}
|
||||
<form action="/resources" id='search-form' className="hidden">
|
||||
{questions.map((question, index) => {
|
||||
return <HTMLQuestion key={index} question={question} />
|
||||
})}
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const lastPage = <SearchPage pageNumber={previousPage} />;
|
||||
const currentPage = <SearchPage pageNumber={page} />;
|
||||
const backwards = (previousPage ?? -1) >= page;
|
||||
|
||||
export {
|
||||
GuidedSearch,
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="mx-auto overflow-hidden bg-gradient-to-t from-neutral-900 to-neutral-700 px-4 py-2">
|
||||
<h1 className="font-bold text-gray-300">Search</h1>
|
||||
</div>
|
||||
|
||||
<PageTransition
|
||||
backwards={backwards}
|
||||
key={page}
|
||||
lastPage={lastPage}
|
||||
currentPage={currentPage}
|
||||
/>
|
||||
|
||||
{/** Hidden html */}
|
||||
<form action="/resources" id="search-form" className="hidden">
|
||||
{questions.map((question, index) => {
|
||||
return <HTMLQuestion key={index} question={question} />;
|
||||
})}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { GuidedSearch };
|
||||
|
Reference in New Issue
Block a user