diff --git a/src/Containers/LimitedTimeBanner/LimitedTimeBanner.stories.tsx b/src/Containers/LimitedTimeBanner/LimitedTimeBanner.stories.tsx index 48b3a8881..57fddac47 100644 --- a/src/Containers/LimitedTimeBanner/LimitedTimeBanner.stories.tsx +++ b/src/Containers/LimitedTimeBanner/LimitedTimeBanner.stories.tsx @@ -7,7 +7,7 @@ export default { title: 'Components/LimitedTimeBanner', component: LimitedTimeBanner, args: { - minsRemaining: 120, + minsRemaining: 120, }, } as Meta; diff --git a/src/Containers/LimitedTimeBanner/LimitedTimeBanner.tsx b/src/Containers/LimitedTimeBanner/LimitedTimeBanner.tsx index 4cd4af9c2..10d7a9a90 100644 --- a/src/Containers/LimitedTimeBanner/LimitedTimeBanner.tsx +++ b/src/Containers/LimitedTimeBanner/LimitedTimeBanner.tsx @@ -8,18 +8,18 @@ const MINUTES_IN_YEAR = 524160; export interface LimitedTimeBannerProps extends MainInterface { - /* minutes until time runs out */ - minsRemaining: number, + /* minutes until time runs out */ + minsRemaining?: number, } export const LimitedTimeBanner: React.FC = ({ minsRemaining, ...props }): React.ReactElement => ( - + - {alterTime(minsRemaining)} Remaining + {alterTime(minsRemaining!)} Remaining ); @@ -47,14 +47,14 @@ const alterTime = (value:number) => { return(moment.duration(value,'minutes').humanize()); } -const BannerBox = styled.div` +const BannerBox = styled.div` + width: 300px; + height; 40px; ${({theme}):string => ` font-family: ${theme.font.family}; color: ${theme.colors.background}}; background-color: ${theme.colors.bannerBackgroundColor}; `} - width:350px; - height:40px; `; const Icon = styled(Clock)` diff --git a/src/Containers/LoyaltyPoints/LoyaltyPoints.stories.tsx b/src/Containers/LoyaltyPoints/LoyaltyPoints.stories.tsx new file mode 100644 index 000000000..898ab9309 --- /dev/null +++ b/src/Containers/LoyaltyPoints/LoyaltyPoints.stories.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Meta, Story } from '@storybook/react'; +import { LoyaltyPointsProps, LoyaltyPoints } from '../../index'; + +export default { + + title: 'Components/LoyaltyPoints', + component: LoyaltyPoints, + args: { + loyaltyAmount: 10, + loyaltyPointLimit: 100, + } +} as Meta; + +export const Basic: Story = (args) => ( + +) \ No newline at end of file diff --git a/src/Containers/LoyaltyPoints/LoyaltyPoints.tsx b/src/Containers/LoyaltyPoints/LoyaltyPoints.tsx new file mode 100644 index 000000000..d49d8380a --- /dev/null +++ b/src/Containers/LoyaltyPoints/LoyaltyPoints.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Stars } from '@styled-icons/material/Stars'; + +export interface LoyaltyPointsProps + extends React.HTMLAttributes { + /* The amount of loyalty points displayed on the component */ + loyaltyAmount: number; + /* Limit before loyalty amount displays only this number instead */ + loyaltyPointLimit: number; +} + +export const LoyaltyPoints: React.FC = ({ + loyaltyAmount, + loyaltyPointLimit, + ...props +}): React.ReactElement => +
+{getLoyaltyPoints(loyaltyAmount, loyaltyPointLimit)}
+
; + +function getLoyaltyPoints(loyaltypoints: number, loyaltyPointLimit: number) { + if (loyaltypoints >= loyaltyPointLimit) { + return loyaltyPointLimit + "⁺"; + } + return Math.round(loyaltypoints); +} + +const LoyaltyPointsBox = styled.div` + ${({ theme }): string => ` + font-family: ${theme.font.family}; + font-size: 20px; + width: fit-content; + min-width: 60px; + height: 30px; + border-radius:0px 50px 50px 0px; + background-color: ${theme.colors.background}; + color: ${theme.colors.loyaltyText}; + text-align: center; + `} +` +const Star = styled(Stars)` + width: 20px; + height: 20px; +` \ No newline at end of file diff --git a/src/Containers/MenuItemCard/MenuItemCard.stories.tsx b/src/Containers/MenuItemCard/MenuItemCard.stories.tsx new file mode 100644 index 000000000..37aeea0a9 --- /dev/null +++ b/src/Containers/MenuItemCard/MenuItemCard.stories.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { Meta, Story } from '@storybook/react'; +import { MenuItemCard, MenuItemCardProps, LoyaltyPoints, LimitedTimeBanner, SaleTag } from '../../index'; +import { action } from "@storybook/addon-actions"; + +export default { + title: 'Components/Menu Item Card', + component: MenuItemCard, + subcomponents: { LoyaltyPoints, LimitedTimeBanner, SaleTag }, +} as Meta; + +const Template: Story = (args) => + ; + +export const MenuItemCardBasic = Template.bind({}); +MenuItemCardBasic.args = { + itemImage: 'https://keyassets-p2.timeincuk.net/wp/prod/wp-content/uploads/sites/63/2007/09/Ricotta-cheese-pancakes-with-blackberry-butter.jpg', + itemName: 'Blackberry Pancakes', + itemPrice: 15.99, + itemPriceLimit: 1000, + saleAmount: 5, + loyaltyAmount: 0, + loyaltyPointLimit: 100, + minsRemaining: 0, + cardWasClicked: action("Card was clicked and not sold out!"), + sale: false, + soldOut: false, + animated: true, + flat: false, +}; + +export const MenuItemCardSale = Template.bind({}); +MenuItemCardSale.args = { + itemImage: 'https://keyassets-p2.timeincuk.net/wp/prod/wp-content/uploads/sites/63/2007/09/Ricotta-cheese-pancakes-with-blackberry-butter.jpg', + itemName: 'Blackberry Pancakes', + itemPrice: 15.99, + itemPriceLimit: 1000, + saleAmount: 5, + loyaltyAmount: 20, + loyaltyPointLimit: 100, + minsRemaining: 120, + cardWasClicked: action("Card was clicked and not sold out!"), + sale: true, + soldOut: false, + animated: true, + flat: false, +}; + +export const MenuItemCardSoldOut = Template.bind({}); +MenuItemCardSoldOut.args = { + itemImage: 'https://keyassets-p2.timeincuk.net/wp/prod/wp-content/uploads/sites/63/2007/09/Ricotta-cheese-pancakes-with-blackberry-butter.jpg', + itemName: 'Blackberry Pancakes', + itemPrice: 15.99, + itemPriceLimit: 1000, + saleAmount: 5, + loyaltyAmount: 0, + loyaltyPointLimit: 100, + minsRemaining: 0, + cardWasClicked: action("Card was clicked and not sold out!"), + sale: true, + soldOut: true, + animated: false, + flat: false, +}; + +export const MenuItemCardLongText = Template.bind({}); +MenuItemCardLongText.args = { + itemImage: 'https://keyassets-p2.timeincuk.net/wp/prod/wp-content/uploads/sites/63/2007/09/Ricotta-cheese-pancakes-with-blackberry-butter.jpg', + itemName: 'Super crazy long combo special order with extra sides and drinks', + itemPrice: 2560, + itemPriceLimit: 1000, + saleAmount: 200, + loyaltyAmount: 1500, + loyaltyPointLimit: 1200, + minsRemaining: 24000, + cardWasClicked: action("Card was clicked and not sold out!"), + sale: true, + soldOut: false, + animated: true, + flat: false, +}; + +export const MenuItemCardEmpty = Template.bind({}); +MenuItemCardEmpty.args = { + itemImage: '', + itemName: '', + itemPrice: 0, + itemPriceLimit: 1000, + saleAmount: 0, + loyaltyAmount: 0, + loyaltyPointLimit: 100, + minsRemaining: 0, + sale: false, + soldOut: false, + animated: false, + flat: false, +}; + diff --git a/src/Containers/MenuItemCard/MenuItemCard.tsx b/src/Containers/MenuItemCard/MenuItemCard.tsx new file mode 100644 index 000000000..5caa08db6 --- /dev/null +++ b/src/Containers/MenuItemCard/MenuItemCard.tsx @@ -0,0 +1,251 @@ +import React from 'react'; +import styled from 'styled-components'; +import { MainInterface, ResponsiveInterface } from '@Utils/BaseStyles'; +import { transition } from '@Utils/Mixins'; +import { LoyaltyPoints, LimitedTimeBanner, SaleTag } from '../../index'; + +export interface MenuItemCardProps + extends MainInterface, ResponsiveInterface, React.HTMLAttributes { + /* Loyalty points component */ + LoyaltyPoints?: React.ReactElement; + /* Limited time banner component */ + LimitedTimeBanner?: React.ReactElement; + /* Sale tag component */ + SaleTag?: React.ReactElement; + /* Html link for the item, displayed as 300px x 150px */ + itemImage: string, + /* Name of the product, after 30 characters '...' replaces the rest */ + itemName: string, + /* Price of the item, is reduced by saleAmount if on sale and will be rounded by 1000 if above itemPriceLimit */ + itemPrice: number, + /* Limit before the item is rounded by 1000, should be greater than 1000 */ + itemPriceLimit: number, + /* Controls if the sale style is used and if a sale amount should be reduced */ + sale: boolean; + /* Controls if the item can still be sold, also removes other components from card display */ + soldOut: boolean; + /* Number passed to LoyaltyPoints to display the loyalty points amount */ + loyaltyAmount: number, + /* Limit before loyaltyPoints gets capped */ + loyaltyPointLimit: number, + /* Amount that the item is on sale, reduced from itemPrice when sale is active */ + saleAmount: number, + /* Minutes that the item is remaining for, if 0 then will not display */ + minsRemaining?: number, + /* Add box shadow when hovered over */ + animated?: boolean; + /* Controls the border around the menu item card, animated will still display the border when hovered over */ + flat?: boolean; + /* Handles card clicks when the item is not soldout */ + cardWasClicked?: () => void, +} + +export const MenuItemCard: React.FC = ({ + animated, + flat, + sale, + soldOut, + itemImage, + itemName, + itemPrice, + itemPriceLimit, + saleAmount, + loyaltyAmount, + loyaltyPointLimit, + minsRemaining, + cardWasClicked, + ...props + +}): React.ReactElement => { + /** + * Checks the value of the item and checks for the sale and soldout states, then conditionally renders the item value based on the limit and states + * Item sale amount checks that the price of the sale won't lower the price of the item below 0, if it does, output 0 + * @param sale - Indicates if the item is on sale + * @param soldOut - Indicates if the item is sold out + * @param itemPrice - The price of the item + * @param itemPriceLimit - Limit before using the roundItemValue function that returns rounded by 1000 + * @param saleAmount - Amount reduced from the item if its on sale + */ + function getMenuItemStatus(sale: boolean, soldOut: boolean, itemPrice: number, itemPriceLimit: number, saleAmount: number) { + + let itemSaleAmount = 0; + + if (itemPrice - saleAmount > 0) { + itemSaleAmount = itemPrice - saleAmount + } + + if (itemPrice >= itemPriceLimit) { + return ${roundItemValue(itemPrice, itemPriceLimit)}K+ + } else if (sale) { + return <> + ${itemPrice} + ${itemSaleAmount} + + } else return ${itemPrice} + } + /** + * Checks if item image has a value, then returns it or uses a defualt image + * @param itemImage - URL for the image, if left empty then return default image from below + * @param alt - Full name of the item + */ + function getItemImage(itemImage: string, alt: string) { + if (!!itemImage) { + return + } else return + } + return ( + + + {(!soldOut) && } + + + {(!soldOut && !!loyaltyAmount) && + } + {(!soldOut && sale) && + } + + + {getItemImage(itemImage, itemName)} + + {getStringValue(itemName)} + {soldOut && Sold Out} + + { (!!minsRemaining && !soldOut) && + } + + {(!!itemPrice && !soldOut) && getMenuItemStatus(sale, soldOut, itemPrice, itemPriceLimit, saleAmount)} + + + ); +} +/** + * Checks if the Item Price is greater than or equal to the item price limit + * Returns item price to the nearest tenth rounded + * Also reduces the amount by 1000, the K is added in the component + * @param itemPrice - Current price of the item displayed on the card + * @param itemPriceLimit - A threshold to hit before the item price starts being rounded + */ +function roundItemValue(itemPrice: number, itemPriceLimit: number) { + if (itemPrice >= itemPriceLimit) { + return ((Math.round(itemPrice * .1) / .1) / 1000) + } else return itemPrice; +}; +/** + * Returns the value of the string capped at 30 characters, and adds ... if that limit is hit + * @param itemName + */ +function getStringValue(itemName: string) { + let newString = itemName.substring(0, 30); + if (itemName.length > 30) { + return newString + '...'; + } else + return newString; +} + +const MenuItemCardBox = styled.div` + position: relative; + width: 300px; + height: 250px; + z-index: 1; + cursor: pointer; + box-shadow: ${({ flat, theme }): string => theme.depth[flat ? 0 : 1]}; + + ${({ theme }): string => ` + border-radius: ${theme.dimensions.radius}; + background-color: ${theme.colors.background}; + `} + + ${({ animated, flat, theme, soldOut }): string => + (animated && !soldOut) ? ` ${transition(['box-shadow'])} &:hover { + box-shadow: ${theme.depth[flat ? 1 : 2]}; + }` : ''} + + ${({ soldOut }) => + soldOut && ` + opacity: 0.5; + cursor: not-allowed; `} +`; +const CardClickableDiv = styled.div` + height: 100%; + width: 100%; + position: absolute; +` +const MenuItemCardImage = styled.img` + height: 150px; + width: 300px; + ${({ theme }): string => ` + border-top-left-radius: ${theme.dimensions.radius}; + border-top-right-radius: ${theme.dimensions.radius}; + `} +` +const SoldOutBox = styled.div` + ${({ theme }): string => ` + font-family: ${theme.font.family}; + color: ${theme.colors.background}}; + background-color: ${theme.colors.menuItemCardSoldoutBox}; + border-top-left-radius: ${theme.dimensions.radius}; + border-top-right-radius: ${theme.dimensions.radius}; + `} + position: absolute; + z-index: 2; + top: 0px; + font-size: 25px; + text-align: center; + width: 300px; + height: 40px; + padding-top: 15px; +`; +const MenuItemHeader = styled.header` + font-weight: bolder; + padding-left: 10px; + padding-bottom: 10px; + padding-right: 120px; + font-size: 25px; +` +const PriceText = styled.header` + font-weight: bold; + position: absolute; + font-size: 25px; + text-align: right; + right: 10px; + bottom: 40px; +` +const PriceTextSlash = styled(PriceText)` + text-decoration: line-through; + font-size: 20px; + opacity: .6; + bottom: 60px; + position: absolute; +` +const PriceText1k = styled(PriceText)` + font-size: 25px; + ${({ theme }): string => ` + color: ${theme.colors.ItemCardSaleGreen}; + `} +` +const OnSale = styled.header` + position: absolute; + font-size: 30px; + text-align: right; + font-weight: bold; + ${({ theme }): string => ` + color: ${theme.colors.primary}; + `} + right: 10px; + bottom: 10px; +` +const MenuItemCardAccessoryDiv = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + position: absolute; + width: 100%; + top: 20px; + align-items: center; +` +const LimitedTimeBannerPosition = styled.div` + position: absolute; + top: 110px; +` \ No newline at end of file diff --git a/src/Containers/SaleTag/SaleTag.stories.tsx b/src/Containers/SaleTag/SaleTag.stories.tsx index a5bcd908b..0d730e624 100644 --- a/src/Containers/SaleTag/SaleTag.stories.tsx +++ b/src/Containers/SaleTag/SaleTag.stories.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { Meta, Story } from '@storybook/react'; import { SaleTag, SaleTagProps } from '../../index'; - export default { title: 'Components/SaleTag', component: SaleTag, diff --git a/src/Containers/SaleTag/SaleTag.tsx b/src/Containers/SaleTag/SaleTag.tsx index b65107c13..351213a0f 100644 --- a/src/Containers/SaleTag/SaleTag.tsx +++ b/src/Containers/SaleTag/SaleTag.tsx @@ -16,11 +16,12 @@ export const SaleTag: React.FC = ({ ); -const SaleTagDiv = styled.span` +const SaleTagDiv = styled.div` ${({theme}):string => ` border-radius: 100px; - width: 100px; - height: 40px; + width: fit-content; + min-width: 60px; + height: 20px; padding: 3px 10px; border-style: solid; border-width: 1px; diff --git a/src/Containers/index.ts b/src/Containers/index.ts index c4d13999c..bdf244451 100644 --- a/src/Containers/index.ts +++ b/src/Containers/index.ts @@ -95,3 +95,5 @@ export * from './LimitedTimeBanner/LimitedTimeBanner'; export * from './TreeAccordion/TreeAccordion'; export * from './ReachIndicator/ReachIndicator'; export * from './InfoHeader/InfoHeader'; +export * from './MenuItemCard/MenuItemCard'; +export * from './LoyaltyPoints/LoyaltyPoints'; diff --git a/src/Themes/MainTheme.ts b/src/Themes/MainTheme.ts index 05c6f13d1..90c8f1ba2 100644 --- a/src/Themes/MainTheme.ts +++ b/src/Themes/MainTheme.ts @@ -31,12 +31,15 @@ export interface MainThemeInterface extends ThemeTemplateInterface { chairTableBackground: string; chairOccupiedBackground: string; chairTableEditBackground: string; + menuItemCardSoldoutBox: string; reachIndicatorColors: { red: string; yellow: string; green: string; }; + loyaltyText: string; bannerBackgroundColor: string; + ItemCardSaleGreen: string; }; } @@ -66,7 +69,10 @@ export const MainTheme: MainThemeInterface = { chairTableBackground: '#6c757d', chairOccupiedBackground: '#EE2434', chairTableEditBackground: '#C4C4C4', - bannerBackgroundColor : 'rgba(0,0,0,0.5)', + bannerBackgroundColor: 'rgba(0,0,0,0.5)', + menuItemCardSoldoutBox: '#303030', + loyaltyText: '#0000FF', + ItemCardSaleGreen: '#04CC00', PieChartColors: { Red: '#FF0000', Green: '#008000',