Skip to content

Commit

Permalink
SUM-172 Build calculator component
Browse files Browse the repository at this point in the history
  • Loading branch information
pookmish committed May 2, 2024
1 parent 83a8a12 commit 2f2567d
Show file tree
Hide file tree
Showing 13 changed files with 1,089 additions and 742 deletions.
48 changes: 24 additions & 24 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,38 @@
"@formkit/auto-animate": "^0.8.2",
"@heroicons/react": "^2.1.3",
"@js-temporal/polyfill": "^0.4.4",
"@mui/base": "^5.0.0-beta.42",
"@next/third-parties": "^14.2.2",
"@mui/base": "^5.0.0-beta.43",
"@next/third-parties": "^14.2.3",
"@tailwindcss/container-queries": "^0.1.1",
"@types/node": "^20.12.7",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@types/node": "^20.12.8",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"algoliasearch": "^4.23.3",
"autoprefixer": "^10.4.19",
"axios": "^1.6.8",
"clsx": "^2.1.0",
"decanter": "^7.2.0",
"clsx": "^2.1.1",
"decanter": "^7.3.0",
"drupal-jsonapi-params": "^2.3.1",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.2",
"eslint-config-next": "^14.2.3",
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
"graphql-tag": "^2.12.6",
"html-entities": "^2.5.2",
"html-react-parser": "^5.1.10",
"next": "^14.2.2",
"next": "^14.2.3",
"next-drupal": "^1.6.0",
"postcss": "^8.4.38",
"qs": "^6.12.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-focus-lock": "^2.11.3",
"react-instantsearch": "^7.7.2",
"react-instantsearch-nextjs": "^0.2.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-focus-lock": "^2.12.1",
"react-instantsearch": "^7.7.3",
"react-instantsearch-nextjs": "^0.2.2",
"react-slick": "^0.30.2",
"react-tiny-oembed": "^1.1.0",
"sharp": "^0.33.3",
"tailwind-merge": "^2.2.2",
"tailwind-merge": "^2.3.0",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"usehooks-ts": "^3.1.0"
Expand All @@ -58,23 +58,23 @@
"@graphql-codegen/import-types-preset": "^3.0.0",
"@graphql-codegen/typescript-graphql-request": "^6.2.0",
"@graphql-codegen/typescript-operations": "^4.2.0",
"@next/bundle-analyzer": "^14.2.2",
"@storybook/addon-essentials": "^8.0.8",
"@storybook/addon-interactions": "^8.0.8",
"@storybook/addon-links": "^8.0.8",
"@next/bundle-analyzer": "^14.2.3",
"@storybook/addon-essentials": "^8.0.9",
"@storybook/addon-interactions": "^8.0.9",
"@storybook/addon-links": "^8.0.9",
"@storybook/addon-styling": "^1.3.7",
"@storybook/blocks": "^8.0.8",
"@storybook/nextjs": "^8.0.8",
"@storybook/react": "^8.0.8",
"@storybook/blocks": "^8.0.9",
"@storybook/nextjs": "^8.0.9",
"@storybook/react": "^8.0.9",
"@storybook/testing-library": "^0.2.2",
"@types/react-slick": "^0.23.13",
"concurrently": "^8.2.2",
"encoding": "^0.1.13",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-unused-imports": "^3.1.0",
"eslint-plugin-unused-imports": "^3.2.0",
"react-docgen": "^7.0.3",
"storybook": "^8.0.8",
"storybook": "^8.0.9",
"tsconfig-paths-webpack-plugin": "^4.1.0"
},
"packageManager": "[email protected]"
Expand Down
2 changes: 1 addition & 1 deletion src/components/elements/select-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const SelectList = ({
</div>
}

<ChevronDownIcon width={20} className="flex-shrink-0"/>
<ChevronDownIcon width={20} className="ml-auto flex-shrink-0"/>
</div>
</button>

Expand Down
3 changes: 3 additions & 0 deletions src/components/paragraphs/paragraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ListParagraph from "@components/paragraphs/stanford-lists/list-paragraph"
import {isPreviewMode} from "@lib/drupal/utils";
import {ParagraphUnion} from "@lib/gql/__generated__/drupal.d";
import {Suspense} from "react";
import SumCalculatorParagraph from "@components/paragraphs/sum-calculator/sum-calculator-paragraph";

type Props = {
/**
Expand Down Expand Up @@ -43,6 +44,8 @@ const Paragraph = async ({paragraph}: Props) => {
return <WysiwygParagraph paragraph={paragraph} {...itemProps}/>
case "ParagraphStanfordList":
return <Suspense><ListParagraph paragraph={paragraph} {...itemProps}/></Suspense>
case "ParagraphSumCalculator":
return <SumCalculatorParagraph paragraph={paragraph} {...itemProps}/>
}
console.warn(`Unknown paragraph ${paragraph.__typename}. Item ${paragraph.id}.`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {HtmlHTMLAttributes} from "react";
import {ParagraphSumCalculator} from "@lib/gql/__generated__/drupal.d";
import SumCalculator from "@components/paragraphs/sum-calculator/sum-calculator";

type Props = HtmlHTMLAttributes<HTMLDivElement> & {
paragraph: ParagraphSumCalculator
}

const SumCalculatorParagraph = ({paragraph, ...props}: Props) => {

const costPerStudentType = new Map<string, Map<number, number>>();
costPerStudentType.set("undergraduate", new Map(paragraph.sumCalcUgUnitCost.map(cost => [cost.first as number, cost.second as number])))
costPerStudentType.set("highschool", new Map(paragraph.sumCalcHighUnitCost.map(cost => [cost.first as number, cost.second as number])))
costPerStudentType.set("graduate", new Map(paragraph.sumCalcGradUnitCost.map(cost => [cost.first as number, cost.second as number])))

const appCosts =new Map(paragraph.sumCalcAppFee.map(item => [item.first as string, item.second as number]));
const progCosts =new Map(paragraph.sumCalcProgFee.map(item => [item.first as string, item.second as number]));
const housingCosts =new Map(paragraph.sumCalcHouseFees.map(item => [item.first as string, item.second as number]));

return (
<SumCalculator
costPerStudentTypes={costPerStudentType}
appCosts={appCosts}
progCosts={progCosts}
i20Cost={paragraph.sumCalcI20Fee}
housingCosts={housingCosts}
mealPlanCost={paragraph.sumCalcMeals}
techCost={paragraph.sumCalcTechFee}
mailCost={paragraph.sumCalcMailFee}
insuranceCost={paragraph.sumCalcInsurance}
booksSuppliesCost={paragraph.sumCalcBooks}
healthServiceCost={paragraph.sumCalcHealthFee}
documentsCost={paragraph.sumCalcDocuments}
{...props}
/>
)
}
export default SumCalculatorParagraph
226 changes: 226 additions & 0 deletions src/components/paragraphs/sum-calculator/sum-calculator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
"use client";

import {HtmlHTMLAttributes, useId, useState} from "react";
import SelectList from "@components/elements/select-list";
import {H2, H3} from "@components/elements/headers";
import {useBoolean, useCounter} from "usehooks-ts";
import {formatCurrency} from "@lib/utils/format-currency";
import useAccordion from "@lib/hooks/useAccordion";
import {clsx} from "clsx";
import {ChevronDownIcon} from "@heroicons/react/20/solid";

type Props = HtmlHTMLAttributes<HTMLDivElement> & {
costPerStudentTypes: Map<string, Map<number, number>>
appCosts: Map<string, number>
progCosts: Map<string, number>
i20Cost: number
housingCosts: Map<string, number>
mealPlanCost: number
techCost: number
mailCost: number
insuranceCost: number
booksSuppliesCost: number
healthServiceCost: number
documentsCost: number
}

const SumCalculatorParagraph = ({
costPerStudentTypes,
appCosts,
progCosts,
i20Cost,
housingCosts,
mealPlanCost,
techCost,
mailCost,
insuranceCost,
booksSuppliesCost,
healthServiceCost,
documentsCost,
...props
}: Props) => {
const id = useId();

const [studentType, setStudentType] = useState("")
const {value: needsI20, setValue: setNeedsI20} = useBoolean(false)
const {value: onCampus, setValue: setOnCampus} = useBoolean(false)
const {count: units, setCount: setUnits} = useCounter(0)
const {value: waivingInsurance, setValue: setWaivingInsurance} = useBoolean(true)

const {buttonProps, panelProps, expanded} = useAccordion();

const appFee = appCosts.get(studentType) || 0
const progFee = progCosts.get(studentType) || 0
const i20Fee = needsI20 ? i20Cost : 0
const housingFee = onCampus ? housingCosts.get(studentType) || 0 : 0
const mealPlan = onCampus ? mealPlanCost : 0
const techFee = onCampus ? techCost : 0
const mailFee = onCampus ? mailCost : 0
const insurance = waivingInsurance ? 0 : insuranceCost

const unitsCost = costPerStudentTypes.get(studentType)?.get(units) || 0

const totalCost = appFee + progFee + i20Fee + housingFee + mealPlan + techFee + mailFee + insurance + unitsCost + booksSuppliesCost + healthServiceCost + documentsCost;
const unitOptions: { value: string, label: string }[] = [];

for (let i = 3; i <= 20; i++) {
unitOptions.push({value: `${i}`, label: `${i}`})
}

return (
<div {...props}>
<div className="max-w-7xl mx-auto pb-72">
<div>
<div id={`${id}-type`}>I am a/an _________ student</div>

<SelectList
ariaLabelledby={`${id}-type`}
options={[
{value: "undergraduate", label: "Undergraduate"},
{value: "highschool", label: "High School"},
{value: "graduate", label: "Graduate"}
]}
onChange={(_e, value) => setStudentType(value as string)}
required
/>
</div>

<div>
<div id={`${id}-i20`}>Are you an international student that requires a Stanford issued I-20?</div>

<SelectList
ariaLabelledby={`${id}-i20`}
options={[
{value: "yes", label: "Yes, I am an international student requiring a Stanford Sponsored I-20"},
{value: "no1", label: "No, I am a US citizen or permanent US resident"},
{value: "no2", label: "No, I am an international student with an I-20 sponsored by another institution"}
]}
onChange={(_e, value) => setNeedsI20(value === "yes")}
required
disabled={!studentType}
/>
</div>

<div>
<div id={`${id}-housing`}>Will you be living on campus?</div>
<SelectList
ariaLabelledby={`${id}-housing`}
options={[
{value: "yes", label: "On-Campus"},
{value: "no", label: "Living off campus and commuting"},
]}
onChange={(_e, value) => setOnCampus(value === "yes")}
required
disabled={!studentType}
/>
</div>

<div>
<div id={`${id}-units`}>How many units will you be taking?</div>

<SelectList
ariaLabelledby={`${id}-units`}
options={unitOptions}
onChange={(_e, value) => setUnits(parseInt(value as string))}
required
disabled={!studentType}
/>
</div>

<div>
<div id={`${id}-insurance`}>Will you be waiving Cardinal Care Health Insurance?</div>

<SelectList
ariaLabelledby={`${id}-insurance`}
options={[
{value: "yes", label: "Yes, I will be waiving Cardinal Care."},
{value: "no", label: "No, I would like to stay enrolled in Cardinal Care Health Insurance"},
]}
onChange={(_e, value) => setWaivingInsurance(value === "yes")}
required
disabled={!studentType}
/>

</div>
</div>

<div className="absolute bottom-0 bg-black-10 w-screen left-1/2 -translate-x-1/2">
<div className="max-w-7xl mx-auto">
<H2 className="flex justify-between" aria-live="polite" aria-atomic>
<span>Estimated total cost</span>
<span>{formatCurrency(totalCost)}</span>
</H2>
<p className="text-left">* Disclaimer: this is only an estimate. Actual fees are subject to change.</p>

<div {...panelProps} className={clsx({"hidden": !expanded})}>


{!!units &&
<div>
<div className="flex items-baseline gap-5"><H3>Tuition</H3>(Based on the total number of units)</div>
<SummaryCost label={`${units} Units`} cost={unitsCost}/>
</div>
}

{studentType &&
<div>
<H3>General Fees</H3>
<SummaryCost label="Application Fee" cost={appFee}/>
<SummaryCost label="Program Fee" cost={progFee}/>
<SummaryCost label="Campus Health Services Fee" cost={healthServiceCost}/>
{needsI20 &&
<SummaryCost label="I-20 Processing Fee" cost={i20Fee}/>
}
</div>
}

{onCampus &&
<div>
<H3>On-campus Fees</H3>
<SummaryCost label="Housing Fee" cost={housingFee}/>
<SummaryCost label="Meal Plan" cost={mealPlan}/>
<SummaryCost label="Mail Service Fee" cost={mailFee}/>
<SummaryCost label="Technology Fee " cost={techFee}/>
</div>
}


<div>
<H3>Extra Fees</H3>
<SummaryCost label="Books and Supplies (optional)" cost={booksSuppliesCost}/>
<SummaryCost label="Document Fee" cost={documentsCost}/>
{!waivingInsurance &&
<SummaryCost label="Cardinal Care Health Insurance optional" cost={insurance}/>
}
</div>

<div>
<p className="text-m4 flex justify-between">
<span>Estimated total cost</span> <span>{formatCurrency(totalCost)}</span>
</p>

<p className="text-left">* Disclaimer: this is only an estimate. Actual fees are subject to change.</p>
</div>
</div>

<button {...buttonProps} className="block ml-auto">
<span className="flex items-center">
{expanded ? "Close" : "See"} details
<ChevronDownIcon width={20} className={clsx("transition duration-150", {"rotate-180": expanded})}/>
</span>
</button>
</div>
</div>
</div>
)
}

const SummaryCost = ({label, cost}: { label: string, cost: number }) => {
return (
<div className="flex items-center justify-between">
<span>{label}</span><span>{formatCurrency(cost)}</span>
</div>
)
}

export default SumCalculatorParagraph
Loading

0 comments on commit 2f2567d

Please sign in to comment.