Skip to content

Commit

Permalink
Merge pull request #149 from GunnWATT/lunch-menu
Browse files Browse the repository at this point in the history
Nutrislice integration
  • Loading branch information
33tm authored Sep 29, 2024
2 parents 0bef192 + 11d0b8f commit 7f0a23a
Show file tree
Hide file tree
Showing 16 changed files with 480 additions and 10 deletions.
2 changes: 1 addition & 1 deletion client/src/components/classes/DashboardBlurb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default function DashboardBlurb(props: DashboardBlurbProps) {
<ContentButton onClick={() => setIncludeCompleted(!includeCompleted)}>
{includeCompleted ? 'Hide completed' : 'Show completed'}
</ContentButton>
<Link to="upcoming" className="text-inherit dark:text-inherit no-underline">
<Link to="upcoming" className="text-inherit dark:text-inherit hover:no-underline">
<ContentButton>See more in Upcoming</ContentButton>
</Link>
</div>
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/layout/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export default class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBo
<Wave />
<div className="absolute inset-0 h-max mx-4 md:mx-12 lg:mx-auto my-auto rounded-lg px-8 py-6 lg:max-w-4xl bg-content-secondary shadow-2xl border-t-8 border-theme flex flex-col max-h-[90%]">
<h3 className="font-bold text-2xl mb-3">WATT has crashed!</h3>
<pre className="text-theme text-medium mb-4">
{this.state.error.name} {this.state.error.message}
<pre className="text-theme text-medium whitespace-pre-wrap mb-4">
<strong>{this.state.error.name}:</strong> {this.state.error.message}
</pre>
<pre className="text-secondary text-sm whitespace-pre-wrap break-words overflow-y-auto scrollbar-none mb-3">
{this.state.error.stack}
Expand Down
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 <></>
}
55 changes: 55 additions & 0 deletions client/src/components/lists/MenuModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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={() => nutrition && setNutritionModal(item)}
>
{item}
</div>
{nutrition && (
<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>
)
}
144 changes: 144 additions & 0 deletions client/src/components/lists/NutritionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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>

<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">
{nutrition ? (
<>
<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>
</>
) : (
<p>No nutrition information available.</p>
)}

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

<section className="flex gap-3 flex-wrap justify-end">
<DangerOutlineButton onClick={() => setIsOpen(false)}>
Close
</DangerOutlineButton>
</section>
</CenteredModal>
)
}
2 changes: 1 addition & 1 deletion client/src/components/lists/PillClubComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function PillClubComponent(props: Club & {id: string}) {

return (
<>
<span className="text-sm truncate cursor-pointer px-2.5 py-1 text-secondary rounded-full bg-black/5 dark:bg-black/20 hover:bg-black/10 dark:hover:bg-black/40 transition duration-75" onClick={() => setModal(true)}>
<span className="text-sm truncate cursor-pointer px-2.5 py-1 text-secondary rounded-full bg-black/10 dark:bg-black/20 hover:bg-black/20 dark:hover:bg-black/30 transition duration-75" onClick={() => setModal(true)}>
{name}
</span>
<ClubComponentModal {...props} isOpen={modal} setIsOpen={setModal} />
Expand Down
7 changes: 5 additions & 2 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 @@ -53,7 +54,7 @@ export default function Period(props: PeriodProps) {
<>
<h2 className="text-xl break-words min-w-0">
{id ? (
<a href={`https://pausd.schoology.com/course/${id}`} className="text-black dark:text-white" target="__blank">
<a href={`https://pausd.schoology.com/course/${id}`} className="text-inherit" target="_blank">
{name}
</a>
) : name}
Expand All @@ -80,7 +81,7 @@ export default function Period(props: PeriodProps) {
{({open}) => (<>
<Disclosure.Button className="flex flex-wrap gap-2 items-center mb-2">
{header}
<FiChevronDown className={'h-6 w-6 rounded-full p-1 bg-black/10 dark:bg-black/20' + (open ? ' rotate-180' : '')} />
<FiChevronDown className={'h-6 w-6 rounded-full p-1 bg-black/10 dark:bg-black/20 hover:bg-black/20 dark:hover:bg-black/30' + (open ? ' rotate-180' : '')} />
</Disclosure.Button>
<Disclosure.Panel className="text-secondary bg-black/10 dark:bg-black/20 rounded text-md p-2 -mx-2 mb-2 whitespace-pre-wrap">
{note}
Expand Down Expand Up @@ -108,6 +109,8 @@ export default function Period(props: PeriodProps) {
/>
</div>
)}

<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
Loading

0 comments on commit 7f0a23a

Please sign in to comment.