add prettier lint rule

This commit is contained in:
Brandon Egger 2023-04-18 23:57:57 -05:00
parent e10f2911d9
commit 192c594d4f
20 changed files with 1684 additions and 1182 deletions

View File

@ -4,6 +4,7 @@ const config = {
{ {
extends: [ extends: [
"plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier",
], ],
files: ["*.ts", "*.tsx"], files: ["*.ts", "*.tsx"],
parserOptions: { parserOptions: {
@ -15,9 +16,10 @@ const config = {
parserOptions: { parserOptions: {
project: "./tsconfig.json", project: "./tsconfig.json",
}, },
plugins: ["@typescript-eslint"], plugins: ["@typescript-eslint", "prettier"],
extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
rules: { rules: {
"prettier/prettier": ["error"],
"@typescript-eslint/consistent-type-imports": [ "@typescript-eslint/consistent-type-imports": [
"warn", "warn",
{ {

84
package-lock.json generated
View File

@ -37,6 +37,8 @@
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"eslint": "^8.34.0", "eslint": "^8.34.0",
"eslint-config-next": "^13.2.1", "eslint-config-next": "^13.2.1",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"prettier": "^2.8.1", "prettier": "^2.8.1",
"prettier-plugin-tailwindcss": "^0.2.1", "prettier-plugin-tailwindcss": "^0.2.1",
@ -1803,6 +1805,18 @@
} }
} }
}, },
"node_modules/eslint-config-prettier": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
"integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-import-resolver-node": { "node_modules/eslint-import-resolver-node": {
"version": "0.3.7", "version": "0.3.7",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
@ -2003,6 +2017,27 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/eslint-plugin-prettier": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
"integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
"dev": true,
"dependencies": {
"prettier-linter-helpers": "^1.0.0"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"eslint": ">=7.28.0",
"prettier": ">=2.0.0"
},
"peerDependenciesMeta": {
"eslint-config-prettier": {
"optional": true
}
}
},
"node_modules/eslint-plugin-react": { "node_modules/eslint-plugin-react": {
"version": "7.32.2", "version": "7.32.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz",
@ -2191,6 +2226,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true "dev": true
}, },
"node_modules/fast-diff": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
"dev": true
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.2.12", "version": "3.2.12",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
@ -3874,6 +3915,18 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
} }
}, },
"node_modules/prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"dependencies": {
"fast-diff": "^1.1.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/prettier-plugin-tailwindcss": { "node_modules/prettier-plugin-tailwindcss": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.4.tgz", "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.4.tgz",
@ -6092,6 +6145,13 @@
"eslint-plugin-react-hooks": "^4.5.0" "eslint-plugin-react-hooks": "^4.5.0"
} }
}, },
"eslint-config-prettier": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
"integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
"dev": true,
"requires": {}
},
"eslint-import-resolver-node": { "eslint-import-resolver-node": {
"version": "0.3.7", "version": "0.3.7",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
@ -6251,6 +6311,15 @@
} }
} }
}, },
"eslint-plugin-prettier": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
"integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
"dev": true,
"requires": {
"prettier-linter-helpers": "^1.0.0"
}
},
"eslint-plugin-react": { "eslint-plugin-react": {
"version": "7.32.2", "version": "7.32.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz",
@ -6380,6 +6449,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true "dev": true
}, },
"fast-diff": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
"dev": true
},
"fast-glob": { "fast-glob": {
"version": "3.2.12", "version": "3.2.12",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
@ -7531,6 +7606,15 @@
"integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==",
"dev": true "dev": true
}, },
"prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"requires": {
"fast-diff": "^1.1.2"
}
},
"prettier-plugin-tailwindcss": { "prettier-plugin-tailwindcss": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.4.tgz", "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.4.tgz",

View File

@ -7,6 +7,7 @@
"dev": "next dev", "dev": "next dev",
"postinstall": "prisma generate", "postinstall": "prisma generate",
"lint": "next lint", "lint": "next lint",
"format": "next lint --fix",
"start": "next start", "start": "next start",
"mongo:start": "docker run --rm -d -p 27017:27017 -h $(hostname) --name uiowa_atr_mongo mongo:4.4.3 --replSet=test && sleep 4 && docker exec uiowa_atr_mongo mongo --eval \"rs.initiate();\"" "mongo:start": "docker run --rm -d -p 27017:27017 -h $(hostname) --name uiowa_atr_mongo mongo:4.4.3 --replSet=test && sleep 4 && docker exec uiowa_atr_mongo mongo --eval \"rs.initiate();\""
}, },
@ -42,6 +43,8 @@
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"eslint": "^8.34.0", "eslint": "^8.34.0",
"eslint-config-next": "^13.2.1", "eslint-config-next": "^13.2.1",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"prettier": "^2.8.1", "prettier": "^2.8.1",
"prettier-plugin-tailwindcss": "^0.2.1", "prettier-plugin-tailwindcss": "^0.2.1",

View File

@ -1,9 +1,9 @@
import { type NextPage } from "next/types"; import { type NextPage } from "next/types";
import Image from 'next/image'; import Image from "next/image";
interface QuickLink { interface QuickLink {
label: string, label: string;
href: string, href: string;
} }
const links: QuickLink[] = [ const links: QuickLink[] = [
@ -14,23 +14,29 @@ const links: QuickLink[] = [
{ {
label: "Wendell Johnson", label: "Wendell Johnson",
href: "https://www.facilities.uiowa.edu/named-building/wendell-johnson-speech-and-hearing-center", href: "https://www.facilities.uiowa.edu/named-building/wendell-johnson-speech-and-hearing-center",
} },
] ];
const QuickLink = ({ label, href }: QuickLink) => { const QuickLink = ({ label, href }: QuickLink) => {
return ( return (
<a className="hover:underline space-x-2" target="_blank" href={href}> <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" /> <Image
className="inline"
alt="external link"
width={16}
height={16}
src="/open-external-link-icon.svg"
/>
<span className="inline">{label}</span> <span className="inline">{label}</span>
</a> </a>
) );
}; };
interface ContactInfo { interface ContactInfo {
name: string, name: string;
title: string, title: string;
email?: string, email?: string;
phone?: string, phone?: string;
} }
const contacts: ContactInfo[] = [ const contacts: ContactInfo[] = [
@ -43,67 +49,98 @@ const contacts: ContactInfo[] = [
name: "Eun Kyung (Julie) Jeon", name: "Eun Kyung (Julie) Jeon",
title: "Clinical Assistant Professor", title: "Clinical Assistant Professor",
email: "eunkyung-jeon@uiowa.edu", email: "eunkyung-jeon@uiowa.edu",
phone: "3194671476" phone: "3194671476",
} },
] ];
const ContactInfo = ({ name, title, email, phone }: ContactInfo) => { const ContactInfo = ({ name, title, email, phone }: ContactInfo) => {
return ( return (
<section className="py-4 space-y-2"> <section className="space-y-2 py-4">
<h1 className="text-md">{name}</h1> <h1 className="text-md">{name}</h1>
<p className="italic text-sm text-neutral-400">{title}</p> <p className="text-sm italic text-neutral-400">{title}</p>
{ email ? {email ? (
<section className="space-x-2"> <section className="space-x-2">
<Image className="inline" alt="email" width={20} height={20} src="/mail-icon-white.svg"/> <Image
<h2 className="text-sm inline">{email}</h2> className="inline"
alt="email"
width={20}
height={20}
src="/mail-icon-white.svg"
/>
<h2 className="inline text-sm">{email}</h2>
</section> </section>
: undefined} ) : undefined}
{ phone ? {phone ? (
<section className="space-x-2"> <section className="space-x-2">
<Image className="inline" alt="phone" width={20} height={20} src="/phone-call-icon.svg"/> <Image
<h2 className="text-sm inline">{phone}</h2> className="inline"
alt="phone"
width={20}
height={20}
src="/phone-call-icon.svg"
/>
<h2 className="inline text-sm">{phone}</h2>
</section> </section>
: undefined} ) : undefined}
</section> </section>
) );
} };
const FooterLabeledSection = ({title, children}: { const FooterLabeledSection = ({
title: string, title,
children: JSX.Element[] | JSX.Element, children,
}: {
title: string;
children: JSX.Element[] | JSX.Element;
}) => { }) => {
return ( return (
<div className="flex flex-col px-2 sm:px-8"> <div className="flex flex-col px-2 sm:px-8">
<h1 className="font-bold text-xl text-neutral-400">{title}</h1> <h1 className="text-xl font-bold text-neutral-400">{title}</h1>
{children} {children}
</div> </div>
) );
} };
const Footer: NextPage = () => { const Footer: NextPage = () => {
return ( return (
<div className="w-full bg-neutral-800"> <div className="w-full bg-neutral-800">
{/** yellow stripe */} {/** yellow stripe */}
<div className="bg-yellow-400 border-t-[1px] border-neutral-400 p-[4px]"></div> <div className="border-t-[1px] border-neutral-400 bg-yellow-400 p-[4px]"></div>
{/** Main footer area */} {/** Main footer area */}
<div className="mx-auto max-w-5xl p-4 flex flex-col-reverse md:flex-row justify-between"> <div className="mx-auto flex max-w-5xl flex-col-reverse justify-between p-4 md:flex-row">
{/** Wendell Johnson Info */} {/** Wendell Johnson Info */}
<div className="flex-col mt-8 sm:mt-0"> <div className="mt-8 flex-col sm:mt-0">
<Image alt="University of Iowa logo" width={128} height={64} src="/IOWA-gold-text.png" /> <Image
<div className="px-2 text-neutral-100 space-y-8"> 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> <section>
<h1 className="text-yellow-300 text-md">Communication Sciences and Disorders</h1> <h1 className="text-md text-yellow-300">
<h2 className="text-yellow-100 italic text-sm">College of Liberal Arts and Sciences</h2> Communication Sciences and Disorders
</h1>
<h2 className="text-sm italic text-yellow-100">
College of Liberal Arts and Sciences
</h2>
</section> </section>
<section> <section>
<h3 className="text-sm italic">Wendell Johnson Speech and Hearing Center</h3> <h3 className="text-sm italic">
Wendell Johnson Speech and Hearing Center
</h3>
<p className="text-sm">250 Hawkins Dr</p> <p className="text-sm">250 Hawkins Dr</p>
<p className="text-sm">Iowa City, IA 52242</p> <p className="text-sm">Iowa City, IA 52242</p>
</section> </section>
<section> <section>
<p className="text-sm text-neutral-400 italic"> <p className="text-sm italic text-neutral-400">
Site Designed and Built by <a target="_blank" href="https://brandonegger.com" className="hover:underline"> Site Designed and Built by{" "}
<a
target="_blank"
href="https://brandonegger.com"
className="hover:underline"
>
Brandon Egger Brandon Egger
</a> </a>
</p> </p>
@ -112,29 +149,25 @@ const Footer: NextPage = () => {
</div> </div>
{/** Header and tabs */} {/** Header and tabs */}
<div className="flex flex-row text-neutral-200 mx-auto md:mx-0 sm:px-4 divide-x divide-neutral-500"> <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"> <FooterLabeledSection title="Quick Links">
<div className="flex flex-col pt-4 space-y-2"> <div className="flex flex-col space-y-2 pt-4">
{links.map((quickLink, index) => { {links.map((quickLink, index) => {
return ( return <QuickLink key={index} {...quickLink} />;
<QuickLink key={index} {...quickLink}/>
)
})} })}
</div> </div>
</FooterLabeledSection> </FooterLabeledSection>
<FooterLabeledSection title="Contact"> <FooterLabeledSection title="Contact">
<div className="flex flex-col divide-y divide-neutral-500"> <div className="flex flex-col divide-y divide-neutral-500">
{contacts.map((contactInfo, index) => { {contacts.map((contactInfo, index) => {
return ( return <ContactInfo key={index} {...contactInfo} />;
<ContactInfo key={index} {...contactInfo} />
)
})} })}
</div> </div>
</FooterLabeledSection> </FooterLabeledSection>
</div> </div>
</div> </div>
</div> </div>
) );
} };
export default Footer; export default Footer;

View File

@ -1,7 +1,7 @@
import { type NextPage } from "next"; import { type NextPage } from "next";
import Image from 'next/image'; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { ChevronDownIcon } from '@heroicons/react/24/outline'; import { ChevronDownIcon } from "@heroicons/react/24/outline";
interface DropdownOption { interface DropdownOption {
label: string; label: string;
@ -15,36 +15,54 @@ interface NavBarLinkProps {
} }
const NavBarLink = ({ href, label, dropdown }: NavBarLinkProps) => { const NavBarLink = ({ href, label, dropdown }: NavBarLinkProps) => {
const DropDown = ({dropdownOptions}: {dropdownOptions: DropdownOption[]}) => { const DropDown = ({
dropdownOptions,
}: {
dropdownOptions: DropdownOption[];
}) => {
const options = dropdownOptions.map((dropdownOption, index) => { const options = dropdownOptions.map((dropdownOption, index) => {
return ( return (
<Link key={index} href={dropdownOption.href}> <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> <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> </Link>
) );
}); });
return ( 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"> <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} {options}
</div> </div>
) );
} };
return ( return (
<li className="group relative"> <li className="group relative">
<Link href={href} className={"h-14 block border-neutral-800 box-border" + (dropdown ? "" : " hover:border-b-2")}> <Link
<div className="h-full flex flex-row space-x-[4px]"> href={href}
<div className="inline-block my-auto"> className={
<span className="inline-block font-bold text-lg py-2 align-text-middle">{label}</span> "box-border block h-14 border-neutral-800" +
(dropdown ? "" : " 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">
{label}
</span>
</div> </div>
{dropdown ? <ChevronDownIcon className="w-4" /> : <></>} {dropdown ? <ChevronDownIcon className="w-4" /> : <></>}
</div> </div>
</Link> </Link>
{dropdown && dropdown.length > 0 ? <DropDown dropdownOptions={dropdown} /> : <></>} {dropdown && dropdown.length > 0 ? (
<DropDown dropdownOptions={dropdown} />
) : (
<></>
)}
</li> </li>
); );
} };
const NavBar = () => { const NavBar = () => {
const resourcesDropDown: DropdownOption[] = [ const resourcesDropDown: DropdownOption[] = [
@ -54,48 +72,70 @@ const NavBar = () => {
}, },
{ {
label: "view all", label: "view all",
href: "/resources" href: "/resources",
} },
] ];
return ( 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"> <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 max-w-5xl flex flex-row sm:justify-between px-4"> <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"> <ul id="left-nav-links" className="flex flex-row space-x-10">
<NavBarLink href='/' label='Home'/> <NavBarLink href="/" label="Home" />
<NavBarLink dropdown={resourcesDropDown} href='/resources' label='Resources'/> <NavBarLink
<NavBarLink href='/about' label='About'/> dropdown={resourcesDropDown}
href="/resources"
label="Resources"
/>
<NavBarLink href="/about" label="About" />
</ul> </ul>
<ul id="right-nav-links" className="hidden sm:flex flex-row space-x-10"> <ul id="right-nav-links" className="hidden flex-row space-x-10 sm:flex">
<li className="group relative"> <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"> <a
<div className="h-full flex flex-row space-x-[4px]"> target="_blank"
<div className="inline-block my-auto"> href="https://forms.gle/FD2abgwBuTaipysZ6"
<span className="inline-block font-bold text-lg py-2 align-text-middle">Provide Feedback</span> 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>
</div> </div>
</a> </a>
</li> </li>
<NavBarLink href='/contact' label='Contact Us'/> <NavBarLink href="/contact" label="Contact Us" />
</ul> </ul>
</li> </li>
</nav> </nav>
) );
} };
const Header: NextPage = () => { const Header: NextPage = () => {
return <> 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"> <div
<Image alt="Ear listening" src="/listening-ear.svg" width={64} height={64}/> 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>
<div id="header-title" className="w-64 grid place-items-center"> <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> <h1 className="text-center text-2xl font-bold text-neutral-200">
Center for Auditory Training Resources
</h1>
</div> </div>
</div> </div>
<NavBar /> <NavBar />
</> </>
);
}; };
export default Header; export default Header;

View File

@ -1,31 +1,47 @@
import { type PlatformLink, type PaymentType, type AuditoryResource, type Skill, type SkillLevel, type Manufacturer } from '@prisma/client'; import {
import { CurrencyDollarIcon, ArrowPathRoundedSquareIcon } from '@heroicons/react/24/solid'; type PlatformLink,
import { ClipboardDocumentListIcon } from '@heroicons/react/24/outline'; type PaymentType,
import Image from 'next/image'; type AuditoryResource,
import Link from 'next/link'; type Skill,
import { translateEnumPlatform, translateEnumSkill } from '~/utils/enumWordLut'; type SkillLevel,
import { type ChangeEvent } from 'react'; type Manufacturer,
import { ChevronDownIcon } from '@heroicons/react/24/outline'; } from "@prisma/client";
import { type ParsedUrlQuery, type ParsedUrlQueryInput } from 'querystring'; import {
import { useRouter } from 'next/router'; 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}) => { export const ResourceInfo = ({
resource,
showMoreInfo,
}: {
resource: AuditoryResource;
showMoreInfo?: boolean;
}) => {
const PriceIcons = ({ type }: { type: PaymentType }) => { const PriceIcons = ({ type }: { type: PaymentType }) => {
switch (type) { switch (type) {
case "FREE": { case "FREE": {
return ( return (
<div className="pt-2 space-x-1" title="Free"> <div className="space-x-1 pt-2" title="Free">
<span className="bg-amber-100 italic rounded-lg border border-neutral-900 text-black px-2 py-[1px]"> <span className="rounded-lg border border-neutral-900 bg-amber-100 px-2 py-[1px] italic text-black">
free free
</span> </span>
</div> </div>
) );
} }
case "SUBSCRIPTION_MONTHLY": { case "SUBSCRIPTION_MONTHLY": {
<div className="space-x-1" title="Monthly recurring subscription"> <div className="space-x-1" title="Monthly recurring subscription">
<ArrowPathRoundedSquareIcon className="inline h-6 w-6" /> <ArrowPathRoundedSquareIcon className="inline h-6 w-6" />
<CurrencyDollarIcon className="inline h-6 w-6 text-lime-800" /> <CurrencyDollarIcon className="inline h-6 w-6 text-lime-800" />
</div> </div>;
} }
case "SUBSCRIPTION_WEEKLY": { case "SUBSCRIPTION_WEEKLY": {
return ( return (
@ -33,122 +49,158 @@ export const ResourceInfo = ({resource, showMoreInfo}: {resource: AuditoryResour
<ArrowPathRoundedSquareIcon className="inline h-6 w-6" /> <ArrowPathRoundedSquareIcon className="inline h-6 w-6" />
<CurrencyDollarIcon className="inline h-6 w-6 text-lime-800" /> <CurrencyDollarIcon className="inline h-6 w-6 text-lime-800" />
</div> </div>
) );
}
} }
} }
};
const PlatformInfo = ({platformLinks}: {platformLinks: PlatformLink[]}) => { const PlatformInfo = ({
const platformsStr = platformLinks.map((platformLink) => { platformLinks,
}: {
platformLinks: PlatformLink[];
}) => {
const platformsStr = platformLinks
.map((platformLink) => {
return translateEnumPlatform(platformLink.platform); return translateEnumPlatform(platformLink.platform);
}).join(', '); })
.join(", ");
return <p>{platformsStr}</p>;
};
return ( return (
<p>{platformsStr}</p> <div className="flex flex-row space-x-4 p-4">
) <div className="my-auto h-full">
} {showMoreInfo ? (
return (
<div className="p-4 space-x-4 flex flex-row">
<div className="h-full my-auto">
{showMoreInfo ?
<Link href={`resources/${resource.id}`}> <Link href={`resources/${resource.id}`}>
<div className="w-20 sm:w-28 flex space-y-2 flex-col justify-center"> <div className="flex w-20 flex-col justify-center space-y-2 sm:w-28">
<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}/> <Image
<span className="w-full rounded-xl border border-neutral-400 bg-white drop-shadow-lg"
className="block bg-neutral-900 hover:bg-neutral-500 border border-neutral-900 text-center py-[1px] text-white rounded-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 more info
</span> </span>
</div> </div>
</Link> </Link>
: ) : (
<div className="w-20 sm:w-28 flex space-y-2 flex-col justify-center"> <div className="flex w-20 flex-col justify-center space-y-2 sm:w-28">
<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}/> <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> </div>
<div className="grid place-items-center"> <div className="grid place-items-center">
<div className=""> <div className="">
<h2 className="text-xs italic text-gray-600">{resource.manufacturer?.name}</h2> <h2 className="text-xs italic text-gray-600">
<h1 className="font-bold text-xl">{resource.name}</h1> {resource.manufacturer?.name}
</h2>
<h1 className="text-xl font-bold">{resource.name}</h1>
<PlatformInfo platformLinks={resource.platform_links} /> <PlatformInfo platformLinks={resource.platform_links} />
<PriceIcons type={resource?.payment_options[0] ?? 'FREE'} /> <PriceIcons type={resource?.payment_options[0] ?? "FREE"} />
</div> </div>
</div> </div>
</div> </div>
) );
} };
export const ResourceDescription = ({manufacturer, description}: {manufacturer: null | Manufacturer, description: string}) => { export const ResourceDescription = ({
manufacturer,
description,
}: {
manufacturer: null | Manufacturer;
description: string;
}) => {
return ( return (
<div className="h-full flex flex-col"> <div className="flex h-full flex-col">
{ manufacturer?.required ? {manufacturer?.required ? (
<div className="bg-neutral-600 border-t-[4px] border-neutral-700 p-2"> <div className="border-t-[4px] border-neutral-700 bg-neutral-600 p-2">
<h3 className="text-sm font-bold text-neutral-100">IMPORTANT</h3> <h3 className="text-sm font-bold text-neutral-100">IMPORTANT</h3>
<p className="text-sm text-neutral-300"> <p className="text-sm text-neutral-300">
This resource requires the patient to have a {manufacturer.name} device This resource requires the patient to have a {manufacturer.name}{" "}
device
</p> </p>
</div> </div>
: undefined} ) : undefined}
<div className="p-2"> <div className="p-2">
<p>{description}</p> <p>{description}</p>
</div> </div>
</div> </div>
) );
} };
const ResourceEntry = ({ resource }: { resource: AuditoryResource }) => { const ResourceEntry = ({ resource }: { resource: AuditoryResource }) => {
const ResourceSkills = ({skills, skillLevels}: {skills: Skill[], skillLevels: SkillLevel[]}) => { const ResourceSkills = ({
skills,
skillLevels,
}: {
skills: Skill[];
skillLevels: SkillLevel[];
}) => {
const SkillRanking = ({ skillLevels }: { skillLevels: SkillLevel[] }) => { const SkillRanking = ({ skillLevels }: { skillLevels: SkillLevel[] }) => {
return ( return (
<div className='flex flex-row space-x-2 overflow-x-auto'> <div className="flex flex-row space-x-2 overflow-x-auto">
{skillLevels.includes('BEGINNER') ? {skillLevels.includes("BEGINNER") ? (
<div className="rounded-lg px-[3px] border-green-600 border-2 bg-green-300"> <div className="rounded-lg border-2 border-green-600 bg-green-300 px-[3px]">
<h2 className="text-neutral-900 italic text-sm text-right">Beginner</h2> <h2 className="text-right text-sm italic text-neutral-900">
</div> : undefined Beginner
} </h2>
{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> </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 }) => { const Skill = ({ label }: { label: string }) => {
return ( return (
<li className="space-x-2 flex flex-row px-2 py-[1px]"> <li className="flex flex-row space-x-2 px-2 py-[1px]">
<ClipboardDocumentListIcon className="w-4" /> <ClipboardDocumentListIcon className="w-4" />
<div className="inline"> <div className="inline">
<h3>{label}</h3> <h3>{label}</h3>
</div> </div>
</li> </li>
) );
} };
const skillsComponents = skills.map((skill, index) => { const skillsComponents = skills.map((skill, index) => {
return <Skill key={index} label={translateEnumSkill(skill)}/> return <Skill key={index} label={translateEnumSkill(skill)} />;
}); });
return ( return (
<div className="m-2 flex space-y-4 flex-col"> <div className="m-2 flex flex-col space-y-4">
{ skillsComponents.length > 0 ? {skillsComponents.length > 0 ? (
<div className='rounded-lg bg-gray-100 drop-shadow border border-neutral-900'> <div className="rounded-lg border border-neutral-900 bg-gray-100 drop-shadow">
<ul className="divide-y-2"> <ul className="divide-y-2">{skillsComponents}</ul>
{skillsComponents} </div>
</ul> ) : (
</div> : <></> <></>
} )}
<SkillRanking skillLevels={skillLevels} /> <SkillRanking skillLevels={skillLevels} />
</div> </div>
) );
} };
return ( return (
<tr className="divide-x-[1px] divide-slate-400"> <tr className="divide-x-[1px] divide-slate-400">
@ -156,14 +208,20 @@ const ResourceEntry = ({resource}: {resource: AuditoryResource}) => {
<ResourceInfo showMoreInfo resource={resource} /> <ResourceInfo showMoreInfo resource={resource} />
</td> </td>
<td className="w-1/4 align-top"> <td className="w-1/4 align-top">
<ResourceSkills skills={resource.skills} skillLevels={resource.skill_levels} /> <ResourceSkills
skills={resource.skills}
skillLevels={resource.skill_levels}
/>
</td> </td>
<td className="align-top hidden md:table-cell"> <td className="hidden align-top md:table-cell">
<ResourceDescription manufacturer={resource.manufacturer} description={resource.description} /> <ResourceDescription
manufacturer={resource.manufacturer}
description={resource.description}
/>
</td> </td>
</tr> </tr>
) );
} };
interface PagesNavigationProps { interface PagesNavigationProps {
query?: ParsedUrlQuery; query?: ParsedUrlQuery;
@ -172,7 +230,12 @@ interface PagesNavigationProps {
resultsPerPage: number; resultsPerPage: number;
} }
const PagesNavigation = ({query, currentPage, pageCount, resultsPerPage}: PagesNavigationProps) => { const PagesNavigation = ({
query,
currentPage,
pageCount,
resultsPerPage,
}: PagesNavigationProps) => {
const router = useRouter(); const router = useRouter();
const PageButton = ({ number }: { number: number }) => { const PageButton = ({ number }: { number: number }) => {
const redirectQueryData: ParsedUrlQueryInput = { ...query }; const redirectQueryData: ParsedUrlQueryInput = { ...query };
@ -180,51 +243,60 @@ const PagesNavigation = ({query, currentPage, pageCount, resultsPerPage}: PagesN
return ( return (
<li> <li>
<Link className={"block py px-[9px] m-1 rounded " + (currentPage !== number ? "hover:bg-neutral-400 hover:text-white" : "bg-neutral-800 text-white")} <Link
href={{ pathname: `/resources`, query: {...redirectQueryData} }}> className={
<span className={"text-lg text-center"}>{number}</span> "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> </Link>
</li> </li>
) );
} };
const pages = Array.from(Array(pageCount).keys()).map((pageNumber) => { const pages = Array.from(Array(pageCount).keys()).map((pageNumber) => {
return ( return <PageButton key={pageNumber} number={pageNumber + 1} />;
<PageButton key={pageNumber} number={pageNumber+1} />
)
}); });
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => { const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
if (!query) { if (!query) {
router.push({ router
pathname: '/resources', .push({
pathname: "/resources",
query: { query: {
perPage: event.target.value, perPage: event.target.value,
} },
}).catch((reason) => { })
.catch((reason) => {
console.error(reason); console.error(reason);
}); });
return; return;
} }
query['perPage'] = event.target.value; query["perPage"] = event.target.value;
router.push({ router
pathname: '/resources', .push({
pathname: "/resources",
query: { query: {
...query, ...query,
} },
}).catch((reason) => { })
.catch((reason) => {
console.error(reason); console.error(reason);
}); });
}; };
return ( return (
<div className="flex flex-row justify-between pl-2 pr-4 py-2 bg-amber-100"> <div className="flex flex-row justify-between bg-amber-100 py-2 pl-2 pr-4">
<div className="flex flex-row w-64 space-x-2"> <div className="flex w-64 flex-row space-x-2">
<div className="relative inline-flex"> <div className="relative inline-flex">
<select <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" 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} value={resultsPerPage}
onChange={handleChange} onChange={handleChange}
> >
@ -240,43 +312,55 @@ const PagesNavigation = ({query, currentPage, pageCount, resultsPerPage}: PagesN
<h1 className="text-md"> Results Per Page</h1> <h1 className="text-md"> Results Per Page</h1>
</div> </div>
</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"> <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} {pages}
</ul> </ul>
</div> </div>
) );
} };
const ResourceTable = ({resources, resourcesPerPage, currentPage, totalPages, query}: { const ResourceTable = ({
resources: AuditoryResource[], resources,
resourcesPerPage: number, resourcesPerPage,
currentPage: number, currentPage,
totalPages: number, totalPages,
query: ParsedUrlQuery query,
}: {
resources: AuditoryResource[];
resourcesPerPage: number;
currentPage: number;
totalPages: number;
query: ParsedUrlQuery;
}) => { }) => {
const resourceElements = resources.map((resource, index) => { const resourceElements =
return (<ResourceEntry key={index} resource={resource} />); resources.map((resource, index) => {
return <ResourceEntry key={index} resource={resource} />;
}) ?? []; }) ?? [];
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mx-auto rounded-xl overflow-hidden border border-neutral-400 drop-shadow-md overflow-hidden"> <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} /> <PagesNavigation
<table className="w-full table-fixed bg-neutral-200 border-b border-neutral-400"> 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"> <thead className="bg-gradient-to-t from-neutral-900 to-neutral-700 drop-shadow-md">
<tr> <tr>
<th className="w-1/3 max-w-xs"> <th className="w-1/3 max-w-xs">
<span className="text-gray-300 block px-4 py-2 text-left"> <span className="block px-4 py-2 text-left text-gray-300">
Resource Resource
</span> </span>
</th> </th>
<th className="w-1/4 max-w-xs"> <th className="w-1/4 max-w-xs">
<span className="text-gray-300 block px-4 py-2 text-left"> <span className="block px-4 py-2 text-left text-gray-300">
Skills Skills
</span> </span>
</th> </th>
<th className="hidden md:table-cell"> <th className="hidden md:table-cell">
<span className="text-gray-300 block px-4 py-2 text-left"> <span className="block px-4 py-2 text-left text-gray-300">
Description Description
</span> </span>
</th> </th>
@ -286,9 +370,14 @@ const ResourceTable = ({resources, resourcesPerPage, currentPage, totalPages, qu
{resourceElements} {resourceElements}
</tbody> </tbody>
</table> </table>
{(resources && resources.length > 4) ? {resources && resources.length > 4 ? (
<PagesNavigation query={query} resultsPerPage={resourcesPerPage} currentPage={currentPage} pageCount={totalPages} /> <PagesNavigation
: undefined} query={query}
resultsPerPage={resourcesPerPage}
currentPage={currentPage}
pageCount={totalPages}
/>
) : undefined}
</div> </div>
</div> </div>
); );

View File

@ -1,51 +1,81 @@
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"; 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> { export interface Option<T> {
label: string, label: string;
value: T, value: T;
} }
export interface Question<T> { export interface Question<T> {
for: string, for: string;
header: string, header: string;
question: string, question: string;
maxSelect?: number, maxSelect?: number;
optional: true, optional: true;
options: Option<T>[] options: Option<T>[];
} }
const GreetingPage = ({updatePage}: { const GreetingPage = ({
updatePage: (pageNumber: number) => void, updatePage,
}: {
updatePage: (pageNumber: number) => void;
}) => { }) => {
const getStartedClick = () => { const getStartedClick = () => {
updatePage(1); updatePage(1);
} };
return ( return (
<div className="flex flex-col text-center"> <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> <h1 className="mb-8 max-w-sm text-center text-xl font-extrabold">
<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> Welcome to the auditory training resource search tool!
<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> </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> </div>
) );
} };
/** /**
* Single question component for a guided search * Single question component for a guided search
*/ */
const QuestionPage = ({isLastPage, page, question, updatePage, formData, updateFormData, dontCareData, setDontCareData}: { const QuestionPage = ({
isLastPage: boolean, isLastPage,
page: number, page,
question: Question<QuestionTypes>, question,
updatePage: (pageNumber: number) => void, updatePage,
formData: Record<string, QuestionTypes[]>, formData,
updateFormData: Dispatch<SetStateAction<Record<string, QuestionTypes[]>>>, updateFormData,
dontCareData: Record<string, boolean>, dontCareData,
setDontCareData: Dispatch<SetStateAction<Record<string, boolean>>>, 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;
@ -54,36 +84,55 @@ const QuestionPage = ({isLastPage, page, question, updatePage, formData, updateF
const handleToggle = () => { const handleToggle = () => {
const newFormData = { const newFormData = {
...formData ...formData,
}; };
if (!newFormData[question.for]) { if (!newFormData[question.for]) {
newFormData[question.for] = [option.value]; newFormData[question.for] = [option.value];
} else if (newFormData[question.for]?.includes(option.value)) { } else if (newFormData[question.for]?.includes(option.value)) {
newFormData[question.for] = newFormData[question.for]?.filter(function(item) { newFormData[question.for] =
return item !== option.value newFormData[question.for]?.filter(function (item) {
return item !== option.value;
}) ?? []; }) ?? [];
} else { } else {
newFormData[question.for] = [...newFormData[question.for] ?? [], option.value]; newFormData[question.for] = [
...(newFormData[question.for] ?? []),
option.value,
];
} }
updateFormData(newFormData); updateFormData(newFormData);
} };
if (dontCare) { if (dontCare) {
return ( 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")}> <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} {option.label}
</button> </button>
) );
} }
return ( 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")}> <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} {option.label}
</button> </button>
) );
} };
useEffect(() => { useEffect(() => {
if (!formData[question.for]) { if (!formData[question.for]) {
@ -97,7 +146,7 @@ const QuestionPage = ({isLastPage, page, question, updatePage, formData, updateF
const dontCareToggle = () => { const dontCareToggle = () => {
if (formData[question.for]) { if (formData[question.for]) {
const newFormData = { const newFormData = {
...formData ...formData,
}; };
newFormData[question.for] = []; newFormData[question.for] = [];
@ -105,82 +154,123 @@ const QuestionPage = ({isLastPage, page, question, updatePage, formData, updateF
} }
const newDontCareData = { const newDontCareData = {
...dontCareData ...dontCareData,
} };
newDontCareData[question.for] = !dontCare; newDontCareData[question.for] = !dontCare;
setDontCareData(newDontCareData); setDontCareData(newDontCareData);
} };
const nextClick = () => { const nextClick = () => {
updatePage(page + 1); updatePage(page + 1);
} };
const backClick = () => { const backClick = () => {
updatePage(page - 1); updatePage(page - 1);
} };
const AdvanceButtons = () => { const AdvanceButtons = () => {
return ( return (
<section className=""> <section className="">
{!isLastPage ? {!isLastPage ? (
<div className="space-x-4"> <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
<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> 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>
: ) : (
<div className="flex flex-col space-y-2 mt-4"> <div className="mt-4 flex flex-col space-y-2">
<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
<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> 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> </div>
} )}
</section> </section>
) );
}; };
return ( return (
<div className="h-full flex flex-col justify-between text-center"> <div className="flex h-full flex-col justify-between text-center">
<section className="mt-4"> <section className="mt-4">
<h2 className="text-neutral-400 italic text-xl">{question.header}</h2> <h2 className="text-xl italic text-neutral-400">{question.header}</h2>
<h1 className="text-neutral-900 font-bold text-xl">{question.question}</h1> <h1 className="text-xl font-bold text-neutral-900">
<h3 className="text-neutral-600 text-sm">Select all that apply from below</h3> {question.question}
</h1>
<h3 className="text-sm text-neutral-600">
Select all that apply from below
</h3>
</section> </section>
<section className="flex flex-col space-y-1 justify-center"> <section className="flex flex-col justify-center space-y-1">
{question.options.map((option, index) => { {question.options.map((option, index) => {
return ( return <OptionToggle key={index} option={option} />;
<OptionToggle key={index} option={option} />
);
})} })}
</section> </section>
{question.optional ? {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")}> <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 No Preference
</button> </button>
: undefined} ) : undefined}
<div className="mb-4"> <div className="mb-4">
<AdvanceButtons /> <AdvanceButtons />
</div> </div>
</div> </div>
) );
} };
/** /**
* Wrapper for last and current page to enable the transition animation * Wrapper for last and current page to enable the transition animation
*/ */
const PageTransition = ({backwards, lastPage, currentPage}: { const PageTransition = ({
backwards: boolean, backwards,
lastPage: JSX.Element | null, lastPage,
currentPage: JSX.Element, currentPage,
}: {
backwards: boolean;
lastPage: JSX.Element | null;
currentPage: JSX.Element;
}) => { }) => {
return ( 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
<div className="relative w-1/2 h-full grid place-items-center"> 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} {currentPage}
</div> </div>
{/** last page */} {/** last page */}
<div className="relative w-1/2 h-full grid place-items-center"> <div className="relative grid h-full w-1/2 place-items-center">
{lastPage} {lastPage}
</div> </div>
</div> </div>
@ -191,30 +281,30 @@ const PageTransition = ({backwards, lastPage, currentPage}: {
* Main guided search component. * Main guided search component.
* Page 0 = greeting page. * Page 0 = greeting page.
*/ */
const GuidedSearch = ({questions}: { const GuidedSearch = ({
questions: Question<QuestionTypes>[], questions,
}: {
questions: Question<QuestionTypes>[];
}) => { }) => {
const [page, setPage] = useState<number>(0); const [page, setPage] = useState<number>(0);
const [formData, setFormData] = useState<(Record<string, QuestionTypes[]>)>({}); const [formData, setFormData] = useState<Record<string, QuestionTypes[]>>({});
const [dontCareData, setDoneCareData] = useState<(Record<string, boolean>)>({}); const [dontCareData, setDoneCareData] = useState<Record<string, boolean>>({});
const [previousPage, setPreviousPage] = useState<number | undefined>(undefined); const [previousPage, setPreviousPage] = useState<number | undefined>(
undefined
);
const updatePage = (pageNumber: number) => { const updatePage = (pageNumber: number) => {
setPreviousPage(page); setPreviousPage(page);
setPage(pageNumber); setPage(pageNumber);
}; };
const SearchPage = ({pageNumber}: { const SearchPage = ({ pageNumber }: { pageNumber?: number }) => {
pageNumber?: number,
}) => {
if (pageNumber === undefined) { if (pageNumber === undefined) {
return null; return null;
} }
if (pageNumber === 0) { if (pageNumber === 0) {
return ( return <GreetingPage updatePage={updatePage} />;
<GreetingPage updatePage={updatePage} />
);
} }
const question = questions[pageNumber - 1]; const question = questions[pageNumber - 1];
@ -222,30 +312,46 @@ const GuidedSearch = ({questions}: {
return null; return null;
} }
const isLastPage = pageNumber === questions.length const isLastPage = pageNumber === questions.length;
return ( return (
<QuestionPage dontCareData={dontCareData} setDontCareData={setDoneCareData} isLastPage={isLastPage} page={page} formData={formData} updateFormData={setFormData} updatePage={updatePage} question={question} /> <QuestionPage
dontCareData={dontCareData}
setDontCareData={setDoneCareData}
isLastPage={isLastPage}
page={page}
formData={formData}
updateFormData={setFormData}
updatePage={updatePage}
question={question}
/>
); );
} };
/** /**
* Renders the hidden html form selectors * Renders the hidden html form selectors
*/ */
const HTMLQuestion = ({question}: {question: Question<QuestionTypes>}) => { const HTMLQuestion = ({
question,
}: {
question: Question<QuestionTypes>;
}) => {
return ( return (
<select className="hidden" name={question.for} multiple> <select className="hidden" name={question.for} multiple>
{question.options.map((option, index) => { {question.options.map((option, index) => {
return ( return (
<option key={index} selected={formData[question.for]?.includes(option.value)} value={option.value.toString()}> <option
key={index}
selected={formData[question.for]?.includes(option.value)}
value={option.value.toString()}
>
{option.label} {option.label}
</option> </option>
); );
}) })}
}
</select> </select>
); );
} };
const lastPage = <SearchPage pageNumber={previousPage} />; const lastPage = <SearchPage pageNumber={previousPage} />;
const currentPage = <SearchPage pageNumber={page} />; const currentPage = <SearchPage pageNumber={page} />;
@ -253,22 +359,25 @@ const GuidedSearch = ({questions}: {
return ( return (
<div> <div>
<div className="px-4 py-2 bg-gradient-to-t from-neutral-900 to-neutral-700 mx-auto overflow-hidden"> <div className="mx-auto overflow-hidden bg-gradient-to-t from-neutral-900 to-neutral-700 px-4 py-2">
<h1 className="text-gray-300 font-bold">Search</h1> <h1 className="font-bold text-gray-300">Search</h1>
</div> </div>
<PageTransition backwards={backwards} key={page} lastPage={lastPage} currentPage={currentPage}/> <PageTransition
backwards={backwards}
key={page}
lastPage={lastPage}
currentPage={currentPage}
/>
{/** Hidden html */} {/** Hidden html */}
<form action="/resources" id='search-form' className="hidden"> <form action="/resources" id="search-form" className="hidden">
{questions.map((question, index) => { {questions.map((question, index) => {
return <HTMLQuestion key={index} question={question} /> return <HTMLQuestion key={index} question={question} />;
})} })}
</form> </form>
</div> </div>
) );
} };
export { export { GuidedSearch };
GuidedSearch,
}

View File

@ -16,7 +16,7 @@ const server = z.object({
// Since NextAuth.js automatically uses the VERCEL_URL if present. // Since NextAuth.js automatically uses the VERCEL_URL if present.
(str) => process.env.VERCEL_URL ?? str, (str) => process.env.VERCEL_URL ?? str,
// VERCEL_URL doesn't include `https` so it cant be validated as a URL // VERCEL_URL doesn't include `https` so it cant be validated as a URL
process.env.VERCEL ? z.string().min(1) : z.string().url(), process.env.VERCEL ? z.string().min(1) : z.string().url()
), ),
// Add `.min(1) on ID and SECRET if you want to make sure they're not empty // Add `.min(1) on ID and SECRET if you want to make sure they're not empty
DISCORD_CLIENT_ID: z.string(), DISCORD_CLIENT_ID: z.string(),
@ -70,7 +70,7 @@ if (!!process.env.SKIP_ENV_VALIDATION == false) {
if (parsed.success === false) { if (parsed.success === false) {
console.error( console.error(
"❌ Invalid environment variables:", "❌ Invalid environment variables:",
parsed.error.flatten().fieldErrors, parsed.error.flatten().fieldErrors
); );
throw new Error("Invalid environment variables"); throw new Error("Invalid environment variables");
} }
@ -84,7 +84,7 @@ if (!!process.env.SKIP_ENV_VALIDATION == false) {
throw new Error( throw new Error(
process.env.NODE_ENV === "production" process.env.NODE_ENV === "production"
? "❌ Attempted to access a server-side environment variable on the client" ? "❌ Attempted to access a server-side environment variable on the client"
: `❌ Attempted to access server-side environment variable '${prop}' on the client`, : `❌ Attempted to access server-side environment variable '${prop}' on the client`
); );
return target[/** @type {keyof typeof target} */ (prop)]; return target[/** @type {keyof typeof target} */ (prop)];
}, },

View File

@ -1,3 +1,3 @@
import sslRedirect from 'next-ssl-redirect-middleware'; import sslRedirect from "next-ssl-redirect-middleware";
export default sslRedirect({}); export default sslRedirect({});

View File

@ -15,7 +15,10 @@ const MyApp: AppType<{ session: Session | null }> = ({
<SessionProvider session={session}> <SessionProvider session={session}>
<Head> <Head>
<title>ATR</title> <title>ATR</title>
<meta name="description" content="University of Iowa Center for Auditory Training Resources" /> <meta
name="description"
content="University of Iowa Center for Auditory Training Resources"
/>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<Component {...pageProps} /> <Component {...pageProps} />

View File

@ -1,6 +1,6 @@
import { type NextPage } from "next/types"; import { type NextPage } from "next/types";
import Image from 'next/image'; import Image from "next/image";
import { HandRaisedIcon } from '@heroicons/react/24/solid'; import { HandRaisedIcon } from "@heroicons/react/24/solid";
import Footer from "~/components/Footer"; import Footer from "~/components/Footer";
import Header from "~/components/Header"; import Header from "~/components/Header";
@ -14,17 +14,35 @@ interface Biography {
position: Position; position: Position;
} }
const Biopgraphy = ({bodyName, name, title, body, img, position}: Biography) => { const Biopgraphy = ({
bodyName,
name,
title,
body,
img,
position,
}: Biography) => {
return ( return (
<section className={"sm:space-y-2 p-2 sm:p-4 shadow-xl bg-yellow-100 flex flex-col border-y-2 sm:border-2 sm:rounded-2xl border-neutral-900 col-span-2 overflow-hidden" + (position === 'right' ? " lg:col-start-2 lg:rotate-3" : " lg:-rotate-3")}> <section
<div className="space-x-8 flex flex-row mb-2 items-center"> className={
<Image src={img} alt={`${name} profile`} width={128} height={128} className="shadow-md shadow-neutral-600/50 rounded-lg border border-neutral-900" /> "col-span-2 flex flex-col overflow-hidden border-y-2 border-neutral-900 bg-yellow-100 p-2 shadow-xl sm:space-y-2 sm:rounded-2xl sm:border-2 sm:p-4" +
(position === "right" ? " lg:col-start-2 lg:rotate-3" : " lg:-rotate-3")
}
>
<div className="mb-2 flex flex-row items-center space-x-8">
<Image
src={img}
alt={`${name} profile`}
width={128}
height={128}
className="rounded-lg border border-neutral-900 shadow-md shadow-neutral-600/50"
/>
<div className=""> <div className="">
<h1 className="text-2xl font-bold">{name}</h1> <h1 className="text-2xl font-bold">{name}</h1>
<h2 className="text-neutral-600 italic">{title}</h2> <h2 className="italic text-neutral-600">{title}</h2>
</div> </div>
</div> </div>
<div className="col-span-2 p-2 bg-white rounded-lg border border-neutral-900"> <div className="col-span-2 rounded-lg border border-neutral-900 bg-white p-2">
{bodyName} {body} {bodyName} {body}
</div> </div>
</section> </section>
@ -47,54 +65,66 @@ const biographies: Biography[] = [
body: "is a Clinical Assistant Professor in Communication Sciences and Disorders at the University of Iowa. She earned both her Au.D. and Ph.D. degrees from the University of Iowa in 2008 and 2016, respectively. Dr. Jeon's research and clinical interests focus on aural (re)habilitation for children and adults with hearing aids and cochlear implants. She has served as a reviewer for various journals, including Ear and Hearing and Cochlear Implant International. Dr. Jeon is an active member of several professional organizations, such as the American Academy of Audiology (AAA), the American Cochlear Implant Alliance (ACIA), the American Speech-Language-Hearing Association (ASHA), the Iowa Speech-Language-Hearing Association (ISHA), and the Asia Pacific Society of Speech-Language-Hearing (APSSLH). Currently, she has been entrusted with various leadership responsibilities, including educational committee officer for the APSSLH, Member-at-Large for the Iowa Speech Language Hearing Foundation, Iowa EHDI Advisory Board representative for the ISHA, and program committee member for the CI2023 conference.", body: "is a Clinical Assistant Professor in Communication Sciences and Disorders at the University of Iowa. She earned both her Au.D. and Ph.D. degrees from the University of Iowa in 2008 and 2016, respectively. Dr. Jeon's research and clinical interests focus on aural (re)habilitation for children and adults with hearing aids and cochlear implants. She has served as a reviewer for various journals, including Ear and Hearing and Cochlear Implant International. Dr. Jeon is an active member of several professional organizations, such as the American Academy of Audiology (AAA), the American Cochlear Implant Alliance (ACIA), the American Speech-Language-Hearing Association (ASHA), the Iowa Speech-Language-Hearing Association (ISHA), and the Asia Pacific Society of Speech-Language-Hearing (APSSLH). Currently, she has been entrusted with various leadership responsibilities, including educational committee officer for the APSSLH, Member-at-Large for the Iowa Speech Language Hearing Foundation, Iowa EHDI Advisory Board representative for the ISHA, and program committee member for the CI2023 conference.",
img: "/profiles/jeon-eunkyung.jpg", img: "/profiles/jeon-eunkyung.jpg",
position: "left", position: "left",
} },
] ];
const About: NextPage = () => { const About: NextPage = () => {
return (
return <> <>
<Header /> <Header />
<main> <main>
<div style={{ <div
style={{
backgroundImage: `url("/backdrops/uiowa-aerial.jpeg")`, backgroundImage: `url("/backdrops/uiowa-aerial.jpeg")`,
backgroundPosition: `center`, backgroundPosition: `center`,
}} className="h-96"> }}
<div style={{ className="h-96"
>
<div
style={{
WebkitBackdropFilter: `blur(5px) contrast(50%)`, WebkitBackdropFilter: `blur(5px) contrast(50%)`,
backdropFilter: `blur(5px) contrast(50%)`, backdropFilter: `blur(5px) contrast(50%)`,
}} className="h-full w-full grid place-items-center"> }}
className="grid h-full w-full place-items-center"
>
<div className="space-y-8"> <div className="space-y-8">
<h1 className="mx-auto text-center font-extrabold text-5xl max-w-lg text-yellow-200">About Us</h1> <h1 className="mx-auto max-w-lg text-center text-5xl font-extrabold text-yellow-200">
About Us
</h1>
</div> </div>
</div> </div>
</div> </div>
<div className="w-full"> <div className="w-full">
<div style={{ <div
style={{
backgroundImage: `url("/backdrops/foot-steps.png")`, backgroundImage: `url("/backdrops/foot-steps.png")`,
}} className="mx-auto max-w-7xl"> }}
<div style={{ className="mx-auto max-w-7xl"
>
<div
style={{
WebkitBackdropFilter: `blur(2px)`, WebkitBackdropFilter: `blur(2px)`,
backdropFilter: `blur(2px)`, backdropFilter: `blur(2px)`,
}} className="sm:p-8"> }}
className="sm:p-8"
>
{/** Small screens */} {/** Small screens */}
<div className="sm:hidden w-full bg-neutral-900 p-4 border-b-2 border-yellow-400"> <div className="w-full border-b-2 border-yellow-400 bg-neutral-900 p-4 sm:hidden">
<h1 className="text-white text-4xl font-bold text-center"> <h1 className="text-center text-4xl font-bold text-white">
Meet the Team Meet the Team
<HandRaisedIcon className="ml-4 rotate-12 text-yellow-200 inline w-12 animate-hand_wave animate-hand_pop"/> <HandRaisedIcon className="animate-hand_pop ml-4 inline w-12 rotate-12 animate-hand_wave text-yellow-200" />
</h1> </h1>
</div> </div>
{/** Large screens */} {/** Large screens */}
<div className="hidden sm:block mx-auto bg-neutral-900 p-4 mx-auto w-max rounded-xl mt-8 mb-20 border-2 border-neutral-300 shadow-xl"> <div className="mx-auto mx-auto mt-8 mb-20 hidden w-max rounded-xl border-2 border-neutral-300 bg-neutral-900 p-4 shadow-xl sm:block">
<h1 className="text-white text-4xl font-bold text-center"> <h1 className="text-center text-4xl font-bold text-white">
Meet the Team Meet the Team
<HandRaisedIcon className="ml-4 rotate-12 text-yellow-200 inline w-12 animate-hand_wave"/> <HandRaisedIcon className="ml-4 inline w-12 rotate-12 animate-hand_wave text-yellow-200" />
</h1> </h1>
</div> </div>
<div className="sm:my-16 grid grid-cols-2 lg:grid-cols-3 sm:mt-4 lg:space-y-24 sm:space-y-12"> <div className="grid grid-cols-2 sm:my-16 sm:mt-4 sm:space-y-12 lg:grid-cols-3 lg:space-y-24">
{biographies.map((biography, index) => { {biographies.map((biography, index) => {
return ( return <Biopgraphy key={index} {...biography} />;
<Biopgraphy key={index} {...biography} />
);
})} })}
</div> </div>
</div> </div>
@ -103,6 +133,7 @@ const About: NextPage = () => {
</main> </main>
<Footer /> <Footer />
</> </>
);
}; };
export default About export default About;

View File

@ -12,7 +12,7 @@ export default createNextApiHandler({
env.NODE_ENV === "development" env.NODE_ENV === "development"
? ({ path, error }) => { ? ({ path, error }) => {
console.error( console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`, `❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
); );
} }
: undefined, : undefined,

View File

@ -3,14 +3,13 @@ import Footer from "~/components/Footer";
import Header from "~/components/Header"; import Header from "~/components/Header";
const Contact: NextPage = () => { const Contact: NextPage = () => {
return (
return <> <>
<Header /> <Header />
<main> <main></main>
</main>
<Footer /> <Footer />
</> </>
);
}; };
export default Contact export default Contact;

View File

@ -1,41 +1,53 @@
import { type NextPage } from "next"; import { type NextPage } from "next";
import Link from "next/link"; import Link from "next/link";
import { ArrowUpRightIcon } from '@heroicons/react/20/solid'; import { ArrowUpRightIcon } from "@heroicons/react/20/solid";
import Footer from "~/components/Footer"; import Footer from "~/components/Footer";
import Header from "~/components/Header"; import Header from "~/components/Header";
const TextLink = ({href, children}: { const TextLink = ({ href, children }: { href: string; children: string }) => {
href: string,
children: string,
}) => {
return ( return (
<Link href={href} className="inline-block text-sm align-items-center hover:bg-neutral-900 hover:text-white border border-neutral-900 rounded-md py-[2px] px-[4px]"> <Link
href={href}
className="align-items-center inline-block rounded-md border border-neutral-900 py-[2px] px-[4px] text-sm hover:bg-neutral-900 hover:text-white"
>
{children} {children}
<ArrowUpRightIcon className="inline-block w-4" /> <ArrowUpRightIcon className="inline-block w-4" />
</Link> </Link>
) );
} };
const Home: NextPage = () => { const Home: NextPage = () => {
return ( return (
<> <>
<div className="min-h-screen flex flex-col"> <div className="flex min-h-screen flex-col">
<Header /> <Header />
<div style={{ <div
style={{
backgroundImage: `url("/backdrops/patient-clinic-bg.jpeg")`, backgroundImage: `url("/backdrops/patient-clinic-bg.jpeg")`,
backgroundPosition: `center`, backgroundPosition: `center`,
backgroundRepeat: `no-repeat`, backgroundRepeat: `no-repeat`,
}} className="grow flex flex-col"> }}
<div style={{ className="flex grow flex-col"
>
<div
style={{
WebkitBackdropFilter: `blur(15px) contrast(50%)`, WebkitBackdropFilter: `blur(15px) contrast(50%)`,
backdropFilter: `blur(15px) contrast(50%)`, backdropFilter: `blur(15px) contrast(50%)`,
}} className="grow min-h-[350px] w-full flex flex-col"> }}
<div className="space-y-8 my-auto h-min"> className="flex min-h-[350px] w-full grow flex-col"
<h1 className="mx-auto text-center font-extrabold text-4xl max-w-lg text-yellow-200">Welcome to the Resource Center for Auditory Training!</h1> >
<div className="flex flex-col w-[350px] sm:w-[400px] mx-auto p-4 bg-neutral-900 border space-y-4 border-neutral-500 rounded-md shadow-lg shadow-neutral-800/50"> <div className="my-auto h-min space-y-8">
<p className="text-2xl text-center text-neutral-100">Looking for resource recommendations?</p> <h1 className="mx-auto max-w-lg text-center text-4xl font-extrabold text-yellow-200">
<Link href="/resources/search" className="flex flex-inline font-bold border border-neutral-300 mx-auto bg-yellow-400 hover:bg-yellow-100 p-2 rounded-md animate-expand_in_out"> Welcome to the Resource Center for Auditory Training!
</h1>
<div className="mx-auto flex w-[350px] flex-col space-y-4 rounded-md border border-neutral-500 bg-neutral-900 p-4 shadow-lg shadow-neutral-800/50 sm:w-[400px]">
<p className="text-center text-2xl text-neutral-100">
Looking for resource recommendations?
</p>
<Link
href="/resources/search"
className="flex-inline mx-auto flex animate-expand_in_out rounded-md border border-neutral-300 bg-yellow-400 p-2 font-bold hover:bg-yellow-100"
>
Search for Auditory Resources Search for Auditory Resources
<ArrowUpRightIcon className="inline w-5" /> <ArrowUpRightIcon className="inline w-5" />
</Link> </Link>
@ -45,54 +57,99 @@ const Home: NextPage = () => {
</div> </div>
</div> </div>
<main> <main>
<section className="min-h-[300px] grid place-items-center p-4 sm:p-12 bg-yellow-100 text-white drop-shadow-md border border-b border-neutral-400"> <section className="grid min-h-[300px] place-items-center border border-b border-neutral-400 bg-yellow-100 p-4 text-white drop-shadow-md sm:p-12">
<div className="max-w-5xl flex justify-center flex-col-reverse md:flex-row md:divide-y-0 md:divide-x divide-neutral-700"> <div className="flex max-w-5xl flex-col-reverse justify-center divide-neutral-700 md:flex-row md:divide-y-0 md:divide-x">
<section className="px-4 text-neutral-800"> <section className="px-4 text-neutral-800">
<p className="pt-2">You can use the <TextLink href="/resources">Resources</TextLink> tab to scroll through the list of all resources, or the <TextLink href="/resources/search">Search</TextLink> tab to filter resources based on your preferences and skill level. We also have an <TextLink href="/about">About</TextLink> tab to learn more about the creators of this website and a <TextLink href="/contact">Contact Us</TextLink> tab to submit any comments or questions you have about the site and auditory training in general. We hope you find this website helpful in your auditory training journey, and we are here to support you every step of the way.</p> <p className="pt-2">
You can use the <TextLink href="/resources">Resources</TextLink>{" "}
tab to scroll through the list of all resources, or the{" "}
<TextLink href="/resources/search">Search</TextLink> tab to
filter resources based on your preferences and skill level. We
also have an <TextLink href="/about">About</TextLink> tab to
learn more about the creators of this website and a{" "}
<TextLink href="/contact">Contact Us</TextLink> tab to submit
any comments or questions you have about the site and auditory
training in general. We hope you find this website helpful in
your auditory training journey, and we are here to support you
every step of the way.
</p>
</section> </section>
<h1 className="grow h-full my-auto pr-auto text-4xl font-bold p-4 text-center text-black">Getting Started</h1> <h1 className="pr-auto my-auto h-full grow p-4 text-center text-4xl font-bold text-black">
Getting Started
</h1>
</div> </div>
</section> </section>
<section className="min-h-[300px] grid place-items-center p-4 sm:p-12 bg-neutral-900 text-white border-t border-y border-yellow-200"> <section className="grid min-h-[300px] place-items-center border-y border-t border-yellow-200 bg-neutral-900 p-4 text-white sm:p-12">
<div className="my-auto max-w-5xl flex flex-col md:flex-row divide-y md:divide-y-0 md:divide-x divide-white mx-auto"> <div className="my-auto mx-auto flex max-w-5xl flex-col divide-y divide-white md:flex-row md:divide-y-0 md:divide-x">
<h1 className="grow h-full my-auto pr-auto text-4xl p-4 font-bold text-center text-yellow-200">Our Purpose</h1> <h1 className="pr-auto my-auto h-full grow p-4 text-center text-4xl font-bold text-yellow-200">
Our Purpose
</h1>
<section className="px-4 text-neutral-300"> <section className="px-4 text-neutral-300">
<p className="pt-2">The goal of this site is to provide resources for cochlear implant users to practice listening with their device. While cochlear implants are highly effective in providing access to speech sounds for patients, it can take time and practice for them to adjust to the new signal transmitted through the implant. Auditory training can assist cochlear implant users in practicing listening to environmental sounds, understanding speech sounds in both quiet and noisy environments, and (re)training to enjoy music. We have compiled and categorized all available auditory training resources for cochlear implant users, including smartphone, web-based, and other online resources that can be utilized as clinician-guided or home-based by the cochlear implant patient and their families. Our online index allows both professionals and patients to filter through the resources based on specific characteristics and requested features, making the selection process more efficient.</p> <p className="pt-2">
The goal of this site is to provide resources for cochlear
implant users to practice listening with their device. While
cochlear implants are highly effective in providing access to
speech sounds for patients, it can take time and practice for
them to adjust to the new signal transmitted through the
implant. Auditory training can assist cochlear implant users in
practicing listening to environmental sounds, understanding
speech sounds in both quiet and noisy environments, and
(re)training to enjoy music. We have compiled and categorized
all available auditory training resources for cochlear implant
users, including smartphone, web-based, and other online
resources that can be utilized as clinician-guided or home-based
by the cochlear implant patient and their families. Our online
index allows both professionals and patients to filter through
the resources based on specific characteristics and requested
features, making the selection process more efficient.
</p>
</section> </section>
</div> </div>
</section> </section>
<div className="max-w-5xl mx-auto p-4 sm:p-12 mb-12"> <div className="mx-auto mb-12 max-w-5xl p-4 sm:p-12">
<h1 className="font-extrabold text-2xl sm:text-4xl text-center">Want to learn more?</h1> <h1 className="text-center text-2xl font-extrabold sm:text-4xl">
<div className="flex flex-col pt-8 space-y-6 mx-auto w-fit"> Want to learn more?
<section className="space-x-4 flex flex-row justify-between"> </h1>
<h2 className="italic text-md my-auto inline-block sm:text-lg"> <div className="mx-auto flex w-fit flex-col space-y-6 pt-8">
<section className="flex flex-row justify-between space-x-4">
<h2 className="text-md my-auto inline-block italic sm:text-lg">
Learn more about the project Learn more about the project
</h2> </h2>
<span className="hidden sm:block grow border border-dashed border-neutral-400 h-[1px] my-auto" /> <span className="my-auto hidden h-[1px] grow border border-dashed border-neutral-400 sm:block" />
<Link href="/about" className="font-semibold align-middle inline-block ease-out duration-200 hover:shadow-md hover:bg-yellow-300 shadow-lg shadow-black/50 px-4 py-2 bg-yellow-200 rounded-md border border-neutral-900"> <Link
href="/about"
className="inline-block rounded-md border border-neutral-900 bg-yellow-200 px-4 py-2 align-middle font-semibold shadow-lg shadow-black/50 duration-200 ease-out hover:bg-yellow-300 hover:shadow-md"
>
About About
<ArrowUpRightIcon className="inline-block align-middle w-5" /> <ArrowUpRightIcon className="inline-block w-5 align-middle" />
</Link> </Link>
</section> </section>
<section className="space-x-4 flex flex-row justify-between"> <section className="flex flex-row justify-between space-x-4">
<h2 className="italic text-md my-auto inline-block sm:text-lg"> <h2 className="text-md my-auto inline-block italic sm:text-lg">
Get in touch with the team Get in touch with the team
</h2> </h2>
<span className="hidden sm:block grow border border-dashed border-neutral-400 h-[1px] my-auto" /> <span className="my-auto hidden h-[1px] grow border border-dashed border-neutral-400 sm:block" />
<Link href="/contact" className="font-semibold align-middle inline-block ease-out duration-200 hover:shadow-md hover:bg-yellow-300 shadow-lg shadow-black/50 px-4 py-2 bg-yellow-200 rounded-md border border-neutral-900"> <Link
href="/contact"
className="inline-block rounded-md border border-neutral-900 bg-yellow-200 px-4 py-2 align-middle font-semibold shadow-lg shadow-black/50 duration-200 ease-out hover:bg-yellow-300 hover:shadow-md"
>
Contact Contact
<ArrowUpRightIcon className="inline-block align-middle w-5" /> <ArrowUpRightIcon className="inline-block w-5 align-middle" />
</Link> </Link>
</section> </section>
<section className="space-x-4 flex flex-row justify-between"> <section className="flex flex-row justify-between space-x-4">
<h2 className="italic text-md my-auto inline-block sm:text-lg"> <h2 className="text-md my-auto inline-block italic sm:text-lg">
Tell us how we&rsquo;re doing! Tell us how we&rsquo;re doing!
</h2> </h2>
<a target="_blank" href='https://forms.gle/FD2abgwBuTaipysZ6' className="flex flex-row font-semibold align-middle inline-block ease-out duration-200 hover:shadow-md hover:bg-yellow-300 shadow-lg shadow-black/50 px-4 py-2 bg-yellow-200 rounded-md border border-neutral-900"> <a
target="_blank"
href="https://forms.gle/FD2abgwBuTaipysZ6"
className="inline-block flex flex-row rounded-md border border-neutral-900 bg-yellow-200 px-4 py-2 align-middle font-semibold shadow-lg shadow-black/50 duration-200 ease-out hover:bg-yellow-300 hover:shadow-md"
>
Give Feedback Give Feedback
<ArrowUpRightIcon className="inline align-middle w-5" /> <ArrowUpRightIcon className="inline w-5 align-middle" />
</a> </a>
</section> </section>
</div> </div>

View File

@ -1,35 +1,35 @@
import { type InferGetStaticPropsType, type GetStaticPropsContext } from "next"; import { type InferGetStaticPropsType, type GetStaticPropsContext } from "next";
import { GlobeAltIcon, DocumentIcon } from '@heroicons/react/24/solid'; import { GlobeAltIcon, DocumentIcon } from "@heroicons/react/24/solid";
import { createProxySSGHelpers } from '@trpc/react-query/ssg'; import { createProxySSGHelpers } from "@trpc/react-query/ssg";
import { appRouter } from "~/server/api/root"; import { appRouter } from "~/server/api/root";
import { prisma } from "~/server/db"; import { prisma } from "~/server/db";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { ResourceDescription, ResourceInfo } from "~/components/ResourceTable"; import { ResourceDescription, ResourceInfo } from "~/components/ResourceTable";
import { type PlatformLink } from "@prisma/client"; import { type PlatformLink } from "@prisma/client";
import Image from 'next/image'; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import Footer from "~/components/Footer"; import Footer from "~/components/Footer";
import Header from "~/components/Header"; import Header from "~/components/Header";
export const getStaticPaths = async () => { export const getStaticPaths = async () => {
const resources = (await prisma.auditoryResource.findMany({ const resources = await prisma.auditoryResource.findMany({
select: { select: {
id: true, id: true,
} },
})); });
return { return {
paths: resources.map((resource) => ({ paths: resources.map((resource) => ({
params: { params: {
id: resource.id, id: resource.id,
} },
})), })),
fallback: 'blocking', fallback: "blocking",
} };
}; };
export async function getStaticProps( export async function getStaticProps(
context: GetStaticPropsContext<{ id: string }>, context: GetStaticPropsContext<{ id: string }>
) { ) {
const ssg = createProxySSGHelpers({ const ssg = createProxySSGHelpers({
router: appRouter, router: appRouter,
@ -51,90 +51,112 @@ export async function getStaticProps(
}; };
} }
const PlatformLinkButton = ({platformLink}: {platformLink: PlatformLink}) => { const PlatformLinkButton = ({
platformLink,
}: {
platformLink: PlatformLink;
}) => {
const Inner = () => { const Inner = () => {
switch (platformLink.platform) { switch (platformLink.platform) {
case "APP_ANDROID": { case "APP_ANDROID": {
return ( return (
<Image className="w-full" src={`/google-play-badge.png`} alt={`Download on the Apple AppStore`} width={512} height={216}/> <Image
) className="w-full"
src={`/google-play-badge.png`}
alt={`Download on the Apple AppStore`}
width={512}
height={216}
/>
);
} }
case "APP_IOS": { case "APP_IOS": {
return ( return (
<Image className="w-full" src={`/app-store-badge.png`} alt={`Download on the Apple AppStore`} width={512} height={216}/> <Image
) className="w-full"
src={`/app-store-badge.png`}
alt={`Download on the Apple AppStore`}
width={512}
height={216}
/>
);
} }
case "PDF": { case "PDF": {
return ( return (
<div className="hover:bg-amber-200 bg-amber-300 border-2 px-2 h-16 align-middle border-neutral-900 rounded-lg flex flex-row space-x-2"> <div className="flex h-16 flex-row space-x-2 rounded-lg border-2 border-neutral-900 bg-amber-300 px-2 align-middle hover:bg-amber-200">
<DocumentIcon className="w-6" /> <DocumentIcon className="w-6" />
<span className="font-bold text-sm my-auto"> <span className="my-auto text-sm font-bold">Document</span>
Document
</span>
</div> </div>
) );
} }
case "WEBSITE": { case "WEBSITE": {
return ( return (
<div className="hover:bg-amber-200 bg-amber-300 border-2 px-2 h-14 align-middle border-neutral-900 rounded-lg flex flex-row space-x-2"> <div className="flex h-14 flex-row space-x-2 rounded-lg border-2 border-neutral-900 bg-amber-300 px-2 align-middle hover:bg-amber-200">
<GlobeAltIcon className="w-6" /> <GlobeAltIcon className="w-6" />
<span className="font-bold text-sm my-auto"> <span className="my-auto text-sm font-bold">Website</span>
Website
</span>
</div> </div>
) );
}
} }
} }
};
return ( return (
<Link href={platformLink.link} target="_blank" rel="noopener noreferrer"> <Link href={platformLink.link} target="_blank" rel="noopener noreferrer">
<Inner /> <Inner />
</Link> </Link>
) );
} };
const DownloadButtons = ({platformLinks}: {platformLinks: PlatformLink[]}) => { const DownloadButtons = ({
platformLinks,
}: {
platformLinks: PlatformLink[];
}) => {
const buttons = platformLinks.map((paltformLink, index) => { const buttons = platformLinks.map((paltformLink, index) => {
return ( return <PlatformLinkButton key={index} platformLink={paltformLink} />;
<PlatformLinkButton key={index} platformLink={paltformLink} />
)
}); });
return ( return <div className="mx-auto flex w-48 flex-col space-y-2">{buttons}</div>;
<div className="w-48 mx-auto flex flex-col space-y-2"> };
{buttons}
</div>
)
}
const ResourceViewPage = (props: InferGetStaticPropsType<typeof getStaticProps>) => { const ResourceViewPage = (
props: InferGetStaticPropsType<typeof getStaticProps>
) => {
const { id } = props; const { id } = props;
const resourceQuery = api.auditoryResource.byId.useQuery({ id }); const resourceQuery = api.auditoryResource.byId.useQuery({ id });
if (!resourceQuery.data) { if (!resourceQuery.data) {
return <> return <></>;
</>
} }
return <> return (
<>
<div className="min-h-screen"> <div className="min-h-screen">
<Header /> <Header />
<main className="mb-12"> <main className="mb-12">
<div className="flex py-4 flex-col flex-col-reverse sm:flex-row divide-x max-w-2xl mx-auto"> <div className="mx-auto flex max-w-2xl flex-col flex-col-reverse divide-x py-4 sm:flex-row">
<div className="text-lg flex flex-col justify-end font-bold my-5 mr-4"> <div className="my-5 mr-4 flex flex-col justify-end text-lg font-bold">
<div className="mx-4"> <div className="mx-4">
<h1 className="border-b mb-2 border-neutral-400">Links</h1> <h1 className="mb-2 border-b border-neutral-400">Links</h1>
<DownloadButtons platformLinks={resourceQuery.data.platform_links} /> <DownloadButtons
platformLinks={resourceQuery.data.platform_links}
/>
</div> </div>
</div> </div>
<div className="flex pb-5 flex-col justify-left"> <div className="justify-left flex flex-col pb-5">
<ResourceInfo resource={resourceQuery.data} /> <ResourceInfo resource={resourceQuery.data} />
<div className="mx-4 text-left border border-neutral-400 rounded-xl overflow-hidden bg-neutral-200 shadow"> <div className="mx-4 overflow-hidden rounded-xl border border-neutral-400 bg-neutral-200 text-left shadow">
<ResourceDescription manufacturer={resourceQuery.data.manufacturer} description={resourceQuery.data.description} /> <ResourceDescription
manufacturer={resourceQuery.data.manufacturer}
description={resourceQuery.data.description}
/>
</div> </div>
<div className="ml-4 mt-4 mr-auto border-2 border-neutral-900 rounded-lg bg-neutral-600"> <div className="ml-4 mt-4 mr-auto rounded-lg border-2 border-neutral-900 bg-neutral-600">
<span className="text-neutral-200 text-sm px-2 py-2">Ages {resourceQuery.data.ages.min}{resourceQuery.data.ages.max >= 100 ? "+" : `-${resourceQuery.data.ages.max}`}</span> <span className="px-2 py-2 text-sm text-neutral-200">
Ages {resourceQuery.data.ages.min}
{resourceQuery.data.ages.max >= 100
? "+"
: `-${resourceQuery.data.ages.max}`}
</span>
</div> </div>
</div> </div>
</div> </div>
@ -142,6 +164,7 @@ const ResourceViewPage = (props: InferGetStaticPropsType<typeof getStaticProps>)
<Footer /> <Footer />
</div> </div>
</> </>
);
}; };
export default ResourceViewPage export default ResourceViewPage;

View File

@ -1,4 +1,4 @@
import { LinkIcon } from '@heroicons/react/20/solid'; import { LinkIcon } from "@heroicons/react/20/solid";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import ResourceTable from "~/components/ResourceTable"; import ResourceTable from "~/components/ResourceTable";
@ -8,7 +8,7 @@ import Footer from "~/components/Footer";
import Header from "~/components/Header"; import Header from "~/components/Header";
const Resources = () => { const Resources = () => {
const router = useRouter() const router = useRouter();
const queryData = parseQueryData(router.query); const queryData = parseQueryData(router.query);
const currentPage = queryData.page; const currentPage = queryData.page;
@ -23,7 +23,7 @@ const Resources = () => {
}); });
if (!resourceQuery.data) { if (!resourceQuery.data) {
return <></> return <></>;
} }
const totalPages = Math.ceil(resourceQuery.data.count / queryData.perPage); const totalPages = Math.ceil(resourceQuery.data.count / queryData.perPage);
@ -31,24 +31,35 @@ const Resources = () => {
return ( return (
<> <>
<Header /> <Header />
<main className="my-6 md:px-4 max-w-6xl mx-auto"> <main className="my-6 mx-auto max-w-6xl md:px-4">
<div className="sm:mb-4 mb-2 sm:p-4 p-2 space-y-2"> <div className="mb-2 space-y-2 p-2 sm:mb-4 sm:p-4">
<h1 className="text-3xl font-bold">All Resources</h1> <h1 className="text-3xl font-bold">All Resources</h1>
<div className=""> <div className="">
<p className="inline">Fill out the </p> <p className="inline">Fill out the </p>
<Link href="/resources/search" <Link
className="hover:bg-neutral-900 hover:text-white inline rounded-lg bg-neutral-200 border border-neutral-800 px-2 py-[4px]"> href="/resources/search"
className="inline rounded-lg border border-neutral-800 bg-neutral-200 px-2 py-[4px] hover:bg-neutral-900 hover:text-white"
>
search form search form
<LinkIcon className="w-4 inline" /> <LinkIcon className="inline w-4" />
</Link> </Link>
<p className="inline"> for a list of auditory training resource recommendations.</p> <p className="inline">
{" "}
for a list of auditory training resource recommendations.
</p>
</div> </div>
</div> </div>
<ResourceTable resourcesPerPage={queryData.perPage} resources={resourceQuery.data.resources} totalPages={totalPages} query={router.query} currentPage={currentPage} /> <ResourceTable
resourcesPerPage={queryData.perPage}
resources={resourceQuery.data.resources}
totalPages={totalPages}
query={router.query}
currentPage={currentPage}
/>
</main> </main>
<Footer /> <Footer />
</> </>
); );
} };
export default Resources; export default Resources;

View File

@ -1,6 +1,10 @@
import Footer from "~/components/Footer"; import Footer from "~/components/Footer";
import Header from "~/components/Header"; import Header from "~/components/Header";
import { GuidedSearch, type Question, type QuestionTypes } from "~/components/Search"; import {
GuidedSearch,
type Question,
type QuestionTypes,
} from "~/components/Search";
const questions: Question<QuestionTypes>[] = [ const questions: Question<QuestionTypes>[] = [
{ {
@ -45,8 +49,8 @@ const questions: Question<QuestionTypes>[] = [
{ {
label: "PDF (printable)", label: "PDF (printable)",
value: "PDF", value: "PDF",
} },
] ],
}, },
{ {
for: "skills", for: "skills",
@ -78,7 +82,7 @@ const questions: Question<QuestionTypes>[] = [
label: "Environmental Sounds", label: "Environmental Sounds",
value: "ENVIRONMENT", value: "ENVIRONMENT",
}, },
] ],
}, },
{ {
for: "skill_levels", for: "skill_levels",
@ -97,18 +101,19 @@ const questions: Question<QuestionTypes>[] = [
{ {
label: "Advanced", label: "Advanced",
value: "ADVANCED", value: "ADVANCED",
}
]
}, },
] ],
},
];
const SearchPage = () => { const SearchPage = () => {
return <> return (
<div className="max-h-screen overflow-y-scroll snap snap-y snap-mandatory"> <>
<div className="snap max-h-screen snap-y snap-mandatory overflow-y-scroll">
<div className="snap-start snap-always"> <div className="snap-start snap-always">
<Header /> <Header />
</div> </div>
<div className="snap-center snap-always w-full max-w-xl mx-auto mt-4 mb-4 rounded-xl overflow-hidden border border-neutral-400 bg-neutral-200 drop-shadow-md"> <div className="mx-auto mt-4 mb-4 w-full max-w-xl snap-center snap-always overflow-hidden rounded-xl border border-neutral-400 bg-neutral-200 drop-shadow-md">
<GuidedSearch questions={questions} /> <GuidedSearch questions={questions} />
</div> </div>
<div className="snap-end snap-always"> <div className="snap-end snap-always">
@ -116,6 +121,7 @@ const SearchPage = () => {
</div> </div>
</div> </div>
</> </>
} );
};
export default SearchPage export default SearchPage;

View File

@ -1,9 +1,12 @@
import { SkillLevel, Skill, Platform, type AuditoryResource } from "@prisma/client"; import {
SkillLevel,
Skill,
Platform,
type AuditoryResource,
} from "@prisma/client";
import { z } from "zod"; import { z } from "zod";
import { import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
createTRPCRouter, publicProcedure,
} from "~/server/api/trpc";
export const auditoryResourceRouter = createTRPCRouter({ export const auditoryResourceRouter = createTRPCRouter({
byId: publicProcedure byId: publicProcedure
@ -12,7 +15,7 @@ export const auditoryResourceRouter = createTRPCRouter({
const resource = await ctx.prisma.auditoryResource.findUnique({ const resource = await ctx.prisma.auditoryResource.findUnique({
where: { where: {
id: input.id, id: input.id,
} },
}); });
return { ...resource } as AuditoryResource; return { ...resource } as AuditoryResource;
@ -23,17 +26,21 @@ export const auditoryResourceRouter = createTRPCRouter({
}), }),
search: publicProcedure search: publicProcedure
.input(z.object({ .input(
z.object({
take: z.number().int(), take: z.number().int(),
skip: z.number().int(), skip: z.number().int(),
ages: z.object({ ages: z
.object({
min: z.number().int(), min: z.number().int(),
max: z.number().int(), max: z.number().int(),
}).optional(), })
.optional(),
platforms: z.nativeEnum(Platform).array().optional(), platforms: z.nativeEnum(Platform).array().optional(),
skill_levels: z.nativeEnum(SkillLevel).array().optional(), skill_levels: z.nativeEnum(SkillLevel).array().optional(),
skills: z.nativeEnum(Skill).array().optional(), skills: z.nativeEnum(Skill).array().optional(),
})) })
)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }) => {
const search = { const search = {
ages: { ages: {
@ -43,8 +50,8 @@ export const auditoryResourceRouter = createTRPCRouter({
}, },
max: { max: {
gte: input.ages?.max, gte: input.ages?.max,
} },
} },
}, },
skill_levels: { skill_levels: {
hasEvery: input.skill_levels ?? [], hasEvery: input.skill_levels ?? [],
@ -56,10 +63,10 @@ export const auditoryResourceRouter = createTRPCRouter({
some: { some: {
platform: { platform: {
in: input.platforms, in: input.platforms,
} },
} },
} },
} };
const [count, resources] = await ctx.prisma.$transaction([ const [count, resources] = await ctx.prisma.$transaction([
ctx.prisma.auditoryResource.count({ ctx.prisma.auditoryResource.count({
@ -69,12 +76,12 @@ export const auditoryResourceRouter = createTRPCRouter({
skip: input.skip, skip: input.skip,
take: input.take, take: input.take,
where: search, where: search,
}) }),
]); ]);
return { return {
count, count,
resources, resources,
} };
}), }),
}); });

View File

@ -15,10 +15,10 @@ export const translateEnumPlatform = (value: Platform) => {
return "PDF Document"; return "PDF Document";
} }
case "WEBSITE": { case "WEBSITE": {
return "Website" return "Website";
}
} }
} }
};
/** /**
* Takes a skill enum value and translates it to human text * Takes a skill enum value and translates it to human text
@ -36,16 +36,16 @@ export const translateEnumSkill = (value: Skill) => {
return "Discourse/Complex"; return "Discourse/Complex";
} }
case "MUSIC": { case "MUSIC": {
return "Music Appreciation" return "Music Appreciation";
} }
case "PHONEMES": { case "PHONEMES": {
return "Phonemes"; return "Phonemes";
} }
case "SENTENCES": { case "SENTENCES": {
return "Sentences" return "Sentences";
} }
case "WORDS": { case "WORDS": {
return "Words" return "Words";
}
} }
} }
};

View File

@ -1,4 +1,9 @@
import { type Platform, type RangeInput, type Skill, type SkillLevel } from "@prisma/client"; import {
type Platform,
type RangeInput,
type Skill,
type SkillLevel,
} from "@prisma/client";
import { type ParsedUrlQuery } from "querystring"; import { type ParsedUrlQuery } from "querystring";
export interface ViewDetails { export interface ViewDetails {
@ -7,10 +12,10 @@ export interface ViewDetails {
} }
export interface SearchQuery { export interface SearchQuery {
age?: RangeInput, age?: RangeInput;
platforms?: Platform[], platforms?: Platform[];
skill_levels?: SkillLevel[], skill_levels?: SkillLevel[];
skills?: Skill[], skills?: Skill[];
} }
export type ParsedQueryData = SearchQuery & ViewDetails; export type ParsedQueryData = SearchQuery & ViewDetails;
@ -19,7 +24,7 @@ export const parseQueryData = (query: ParsedUrlQuery): ParsedQueryData => {
const view = { const view = {
page: Number(query["page"] ?? 1), page: Number(query["page"] ?? 1),
perPage: Number(query["perPage"] ?? 10), perPage: Number(query["perPage"] ?? 10),
} };
const filter: SearchQuery = {}; const filter: SearchQuery = {};
if (query["ages"]) { if (query["ages"]) {
@ -44,7 +49,7 @@ export const parseQueryData = (query: ParsedUrlQuery): ParsedQueryData => {
filter.age = { filter.age = {
min: Math.min(...ages), min: Math.min(...ages),
max: Math.max(...ages), max: Math.max(...ages),
} };
} }
if (query["platforms"]) { if (query["platforms"]) {
@ -72,4 +77,4 @@ export const parseQueryData = (query: ParsedUrlQuery): ParsedQueryData => {
} }
return { ...filter, ...view }; return { ...filter, ...view };
} };