Skip to content

Commit

Permalink
menu integration
Browse files Browse the repository at this point in the history
  • Loading branch information
33tm committed Sep 29, 2024
1 parent 7fa3fc3 commit a51ed02
Show file tree
Hide file tree
Showing 7 changed files with 375 additions and 5 deletions.
77 changes: 77 additions & 0 deletions client/src/components/layout/PeriodActionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ReactNode, useContext, useState } from 'react';
import { DateTime } from 'luxon';
import MenuModal from '../lists/MenuModal';
import MenuContext from '../../contexts/MenuContext';


type ActionButtonProps = {
children: ReactNode,
now: boolean,
note?: string,
onClick?: () => void
}
function ActionButton(props: ActionButtonProps) {
const { children, now, note, onClick } = props;
return (
<button
className={`mt-2 w-full px-3.5 py-1.5 right-5 top-0 rounded-md ${note || 'sm:w-fit sm:absolute sm:my-auto sm:h-fit'} ${now ? 'bottom-8' : 'bottom-0'} bg-black/10 dark:bg-black/20 hover:bg-black/20 dark:hover:bg-black/30 transition duration-75`}
onClick={onClick}
>
{children}
</button>
)
}

type PeriodActionButtonProps = {
date: DateTime,
name: string,
now: boolean,
note?: string
}
export default function PeriodActionButton(props: PeriodActionButtonProps) {
const { name } = props;

if (name === 'PRIME' || name === 'Study Hall')
return <FlexiSCHEDAction {...props} />

if (name === 'Brunch' || name === 'Lunch')
return <MenuAction {...props} />

return <></>
}

function FlexiSCHEDAction(props: PeriodActionButtonProps) {
return (
<a href="https://gunn.flexisched.net" target="_blank" className="text-inherit">
<ActionButton {...props}>
FlexiSCHED
</ActionButton>
</a>
)
}

function MenuAction(props: PeriodActionButtonProps) {
const { name, date } = props;
const { menu } = useContext(MenuContext);
const [modal, setModal] = useState(false);

const formatted = date.toFormat('MM-dd');
const meal = name.toLowerCase() as 'brunch' | 'lunch';

if (formatted in menu && menu[formatted][meal])
return (
<>
<ActionButton {...props} onClick={() => setModal(true)}>
Menu
</ActionButton>
<MenuModal
name={name}
items={menu[formatted][meal]}
isOpen={modal}
setIsOpen={setModal}
/>
</>
)

return <></>
}
53 changes: 53 additions & 0 deletions client/src/components/lists/MenuModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useState } from 'react';
import { Dialog } from '@headlessui/react';

import NutritionModal from './NutritionModal';
import CenteredModal from '../layout/CenteredModal';
import { DangerOutlineButton } from '../layout/OutlineButton';

import type { Entry } from '../../contexts/MenuContext';


type MenuModalProps = {
name: string,
items: { [item: string]: Entry },
isOpen: boolean,
setIsOpen: (open: boolean) => void
}
export default function MenuModal(props: MenuModalProps) {
const { name, items, isOpen, setIsOpen } = props;
const [nutritionModal, setNutritionModal] = useState<string | null>(null);

return (
<CenteredModal className="relative flex flex-col bg-content rounded-md w-[28rem] max-h-[90%] mx-2 p-6 shadow-xl" isOpen={isOpen} setIsOpen={setIsOpen}>
<Dialog.Title className="text-xl font-semibold mb-3 pr-6">
{name} Menu
</Dialog.Title>

<section className="mb-4 space-y-1 overflow-scroll scroll-smooth scrollbar-none">
{Object.entries(items).map(([item, nutrition]) => (
<div key={item}>
<div
className="truncate text-center cursor-pointer px-8 py-4 text-secondary rounded-md bg-black/10 dark:bg-black/20 hover:bg-black/20 dark:hover:bg-black/30 transition duration-75"
onClick={() => setNutritionModal(item)}
>
{item}
</div>
<NutritionModal
item={item}
nutrition={nutrition}
isOpen={(nutritionModal === item)}
setIsOpen={() => setNutritionModal(null)}
/>
</div>
))}
</section>

<section className="flex gap-3 flex-wrap justify-end">
<DangerOutlineButton onClick={() => setIsOpen(false)}>
Close
</DangerOutlineButton>
</section>
</CenteredModal>
)
}
142 changes: 142 additions & 0 deletions client/src/components/lists/NutritionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { ReactNode } from 'react';
import { Dialog } from '@headlessui/react';

import CenteredModal from '../layout/CenteredModal';
import { DangerOutlineButton } from '../layout/OutlineButton';

import type { Entry } from '../../contexts/MenuContext';


type ItemProps = {
children: ReactNode,
value?: number,
dv?: number
}
function Item(props: ItemProps) {
const { children, value, dv } = props;

if (value === null)
return <></>

return (
<>
<hr className="my-0.5 opacity-50" />
<div className="flex justify-between">
{children}
{dv && <strong>{Math.floor((value! / dv) * 100)}%</strong>}
</div>
</>
)
}

type NutritionModalProps = {
item: string,
nutrition: Entry,
isOpen: boolean,
setIsOpen: (open: boolean) => void
}
export default function NutritionModal(props: NutritionModalProps) {
const {
item,
nutrition: {
serving,
nutrition,
ingredients
},
isOpen,
setIsOpen
} = props;

return (
<CenteredModal className="relative flex flex-col bg-content rounded-md w-[28rem] max-h-[90%] mx-2 p-6 shadow-xl" isOpen={isOpen} setIsOpen={setIsOpen}>
<Dialog.Title className="text-xl font-semibold mb-3 pr-6">
{item}
</Dialog.Title>

{nutrition ? (
<section className="mb-4 py-2 px-4 rounded-md overflow-scroll scroll-smooth scrollbar-none text-secondary bg-black/10 dark:bg-black/20">
<strong className="text-2xl">Nutrition Facts</strong>
<hr className="my-0.5" />
{serving && serving.serving_size_amount && serving.serving_size_unit && (
<>
<div className="flex justify-between space-x-8">
<strong>Serving size</strong>
<strong>{serving.serving_size_amount} {serving.serving_size_unit}</strong>
</div>
<hr className="my-0.5" />
</>
)}
{nutrition.calories && (
<>
<div className="flex justify-between text-xl">
<strong>Calories</strong>
<strong>{nutrition.calories}</strong>
</div>
<hr className="my-0.5" />
</>
)}
<div className="flex justify-end">
<strong className="text-right">% Daily Value*</strong>
</div>

<Item value={nutrition.g_fat} dv={78}>
<p><strong>Total Fat</strong> {nutrition.g_fat}g</p>
</Item>
<Item value={nutrition.g_saturated_fat} dv={20}>
<p className="ml-4">Saturated Fat {nutrition.g_saturated_fat}g</p>
</Item>
<Item value={nutrition.g_trans_fat}>
<p className="ml-4"><i>Trans</i> Fat {nutrition.g_trans_fat}g</p>
</Item>

<Item value={nutrition.mg_cholesterol} dv={300}>
<p><strong>Cholesterol</strong> {nutrition.mg_cholesterol}mg</p>
</Item>

<Item value={nutrition.mg_sodium} dv={2300}>
<p><strong>Sodium</strong> {nutrition.mg_sodium}mg</p>
</Item>

<Item value={nutrition.g_carbs} dv={275}>
<p><strong>Total Carbohydrate</strong> {nutrition.g_carbs}g</p>
</Item>
<Item value={nutrition.g_fiber} dv={28}>
<p className="ml-4">Dietary Fiber {nutrition.g_fiber}g</p>
</Item>
<Item value={nutrition.g_sugar}>
<p className="ml-4">Total Sugars {nutrition.g_sugar}g</p>
</Item>
<Item value={nutrition.g_added_sugar} dv={50}>
<p className="ml-8">Includes {nutrition.g_added_sugar}g Added Sugars</p>
</Item>

<Item value={nutrition.g_protein} dv={50}>
<p><strong>Protein</strong> {nutrition.g_protein}g</p>
</Item>

<Item>
<p className="text-sm font-light">
* Percent Daily Values are based on a 2,000 calorie diet.
</p>
</Item>

{ingredients && (
<>
<hr className="mt-0.5 mb-1" />
<strong className="text-2xl">Ingredients</strong>
<p>{ingredients}</p>
</>
)}
</section>
) : (
<p>No nutrition information available.</p>
)}

<section className="flex gap-3 flex-wrap justify-end">
<DangerOutlineButton onClick={() => setIsOpen(false)}>
Close
</DangerOutlineButton>
</section>
</CenteredModal>
)
}
5 changes: 2 additions & 3 deletions client/src/components/schedule/Period.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {DateTime} from 'luxon';

// Components
import PillClubComponent from '../lists/PillClubComponent';
import PeriodActionButton from '../layout/PeriodActionButton';

// Contexts
import UserDataContext from '../../contexts/UserDataContext';
Expand Down Expand Up @@ -109,9 +110,7 @@ export default function Period(props: PeriodProps) {
</div>
)}

<button className={`mt-2 w-full px-3.5 py-1.5 right-5 top-0 rounded-md ${note || 'md:w-fit md:absolute md:my-auto md:h-fit'} ${duration.contains(now) ? 'bottom-8' : 'bottom-0'} bg-black/10 dark:bg-black/20 hover:bg-black/20 dark:hover:bg-black/30 transition duration-75`}>
Action
</button>
<PeriodActionButton {...props} date={start} now={duration.contains(now)} />
</div>
);
}
9 changes: 7 additions & 2 deletions client/src/components/schedule/Periods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import NoSchoolImage from './NoSchoolImage';
// Contexts
import CurrentTimeContext from '../../contexts/CurrentTimeContext';
import UserDataContext, {SgyPeriodData, UserData} from '../../contexts/UserDataContext';
import {MenuProvider} from '../../contexts/MenuContext';

// Utils
import {useSchedule} from '../../hooks/useSchedule';
import {useMenu} from '../../hooks/useMenu';
import {periodNameDefault} from '@watt/shared/util/schedule';


Expand All @@ -28,6 +30,9 @@ export default function Periods(props: PeriodsProps) {
const format = userData.options.time === '24' ? 'H:mm' : 'h:mm a';
const classes = userData.classes as {[key: string]: SgyPeriodData};

// Brunch/Lunch menu
const menu = useMenu();

// HTML for a school day, assumes periods is populated
const schoolDay = () => {
// End time of the last period of the day
Expand All @@ -43,7 +48,7 @@ export default function Periods(props: PeriodsProps) {
const displayIndicator = periods && minutes < periods[periods.length - 1].e && minutes >= periods[0].s - 20;

return (
<>
<MenuProvider value={menu}>
<p className="mb-4">
School ends at <strong>{end.toFormat(format)}</strong> today.
</p>
Expand All @@ -62,7 +67,7 @@ export default function Periods(props: PeriodsProps) {
grades={grades}
/>
))}
</>
</MenuProvider>
)
}

Expand Down
52 changes: 52 additions & 0 deletions client/src/contexts/MenuContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createContext } from 'react';


export type Entry = {
serving?: {
serving_size_amount: string,
serving_size_unit: string
},
nutrition?: {
calories?: number,
g_fat?: number,
g_saturated_fat?: number,
g_trans_fat?: number,
mg_cholesterol?: number,
g_carbs?: number,
g_added_sugar?: number,
g_sugar?: number,
mg_potassium?: number,
mg_sodium?: number,
g_fiber?: number,
g_protein?: number,
mg_iron?: number,
mg_calcium?: number,
mg_vitamin_c?: number,
iu_vitamin_a?: number,
re_vitamin_a?: number,
mcg_vitamin_a?: number,
mg_vitamin_d?: number,
mcg_vitamin_d?: number,
},
ingredients?: string
}

export type Menu = {
timestamp: string,
menu: {
[date: string]: {
brunch: { [item: string]: Entry },
lunch: { [item: string]: Entry }
}
}
}

export const defaultMenu: Menu = {
timestamp: new Date().toISOString(),
menu: {}
}

const MenuContext = createContext<Menu>(defaultMenu);

export const MenuProvider = MenuContext.Provider;
export default MenuContext;
Loading

0 comments on commit a51ed02

Please sign in to comment.