From 4bcf8cc32a50b54535b2764c5eb07d9111ab0f8e Mon Sep 17 00:00:00 2001 From: Jacob Wenger Date: Tue, 9 Jul 2024 13:18:33 -0700 Subject: [PATCH] Simplified `/common` component file structure --- .../src/components/common/FootballShape.tsx | 205 ++++++++++++++++++ .../common/FootballShape/index.styles.ts | 123 ----------- .../components/common/FootballShape/index.tsx | 85 -------- ...dex.styles.ts => NewsletterSignupForm.tsx} | 73 ++++++- .../common/NewsletterSignupForm/index.tsx | 61 ------ .../index.tsx => SliderRange.tsx} | 47 +++- .../common/SliderRange/index.styles.ts | 43 ---- .../index.styles.ts => StatsSection.tsx} | 23 +- .../components/common/StatsSection/index.tsx | 19 -- .../index.tsx => YouTubeIcon.tsx} | 33 ++- .../common/YouTubeIcon/index.styles.ts | 25 --- 11 files changed, 363 insertions(+), 374 deletions(-) create mode 100644 website/src/components/common/FootballShape.tsx delete mode 100644 website/src/components/common/FootballShape/index.styles.ts delete mode 100644 website/src/components/common/FootballShape/index.tsx rename website/src/components/common/{NewsletterSignupForm/index.styles.ts => NewsletterSignupForm.tsx} (52%) delete mode 100644 website/src/components/common/NewsletterSignupForm/index.tsx rename website/src/components/common/{SliderRange/index.tsx => SliderRange.tsx} (64%) delete mode 100644 website/src/components/common/SliderRange/index.styles.ts rename website/src/components/common/{StatsSection/index.styles.ts => StatsSection.tsx} (56%) delete mode 100644 website/src/components/common/StatsSection/index.tsx rename website/src/components/common/{YouTubeIcon/index.tsx => YouTubeIcon.tsx} (91%) delete mode 100644 website/src/components/common/YouTubeIcon/index.styles.ts diff --git a/website/src/components/common/FootballShape.tsx b/website/src/components/common/FootballShape.tsx new file mode 100644 index 00000000..843cd188 --- /dev/null +++ b/website/src/components/common/FootballShape.tsx @@ -0,0 +1,205 @@ +import {darken} from 'polished'; +import React from 'react'; +import styled from 'styled-components'; + +import {getColorForResult} from '../../lib/utils'; +import {GameResult} from '../../models'; + +interface FootballShapePathProps { + readonly $type: string; + readonly $gameResult: GameResult | null; + readonly $isSelected: boolean; +} + +const FootballShapePath = styled.path` + cursor: pointer; + stroke-width: 2px; + stroke: ${({theme, $type, $gameResult, $isSelected}) => { + let color; + if ($isSelected) { + color = theme.colors.gold; + } else if ($type === 'past') { + color = $gameResult ? getColorForResult($gameResult) : 'black'; + } else { + color = theme.colors.black; + } + + return darken(0.2, color); + }}; + stroke-dasharray: ${({$type}) => ($type === 'future' ? '4px' : 0)}; + stroke-dashoffset: 3px; +`; + +interface FootballLacesPathProps { + readonly $isSelected: boolean; +} + +const FootballLacesPath = styled.g` + cursor: pointer; + stroke-width: 2px; + stroke-linecap: round; + + line { + stroke: ${({theme, $isSelected}) => { + const color = $isSelected ? theme.colors.gold : theme.colors.black; + return darken(0.2, color); + }}; + } +`; + +interface FootballPatternProps { + readonly $type: string; + readonly $gameResult: GameResult | null; + readonly $isHomeGame: boolean; +} + +const FootballPattern = styled.pattern` + rect { + fill: ${({$type, $gameResult}) => { + if ($type === 'past') { + return $gameResult ? getColorForResult($gameResult) : 'black'; + } else { + return 'transparent'; + } + }}; + } + + line { + stroke: ${({theme, $gameResult, $isHomeGame}) => { + if ($isHomeGame) { + return 'transparent'; + } + + return $gameResult + ? darken(0.2, getColorForResult($gameResult)) + : darken(0.3, theme.colors.lightGray); + }}; + stroke-width: 2px; + } +`; + +interface TextProps { + readonly $gameResult: GameResult | null; + readonly $isSelected: boolean; +} + +const Text = styled.p` + cursor: pointer; + position: absolute; + top: calc(50% - 10px); + color: ${({theme, $gameResult, $isSelected}) => { + if ($isSelected) { + return $gameResult === GameResult.Tie ? darken(0.2, theme.colors.gold) : theme.colors.gold; + } else { + return theme.colors.white; + } + }}; + font-size: 14px; + font-family: 'Inter UI', serif; + width: 100%; +`; + +interface LegProps { + readonly $type: 'left' | 'right'; +} + +const Leg = styled.div` + height: 0; + width: 24px; + border-bottom: solid 2px ${({theme}) => theme.colors.black}; + position: absolute; + bottom: -1; + left: ${({$type}) => ($type === 'left' ? 0 : 'initial')}; + right: ${({$type}) => ($type === 'right' ? 0 : 'initial')}; + transform: rotate(${({$type}) => ($type === 'right' ? 52 : -52)}deg); + + & > div { + margin-top: -4px; + margin-left: 8px; + width: 6px; + height: 10px; + border-left: solid 2px ${({theme}) => theme.colors.black}; + border-right: solid 2px ${({theme}) => theme.colors.black}; + } +`; + +export const FootballShape: React.FC<{ + readonly type: 'past' | 'future'; + readonly text?: string; + readonly title: string; + readonly gameResult: GameResult | null; + readonly legs?: { + readonly left?: string | boolean; + readonly right?: string | boolean; + }; + readonly isHomeGame: boolean; + readonly isSelected: boolean; + readonly uniqueFillPatternId: string; +}> = ({type, text, title, gameResult, legs = {}, isHomeGame, isSelected, uniqueFillPatternId}) => { + let leftLeg: React.ReactNode = null; + if (legs.left) { + leftLeg = {legs.left === 'gap' ?
 
: <> }
; + } + + let rightLeg: React.ReactNode = null; + if (legs.right) { + rightLeg = {legs.right === 'gap' ?
 
: <> }
; + } + + return ( + <> + + {title} + + {/* Football shape. */} + + + {/* Laces. */} + {type === 'future' ? ( + + + + + + + + + ) : null} + + {/* Fill pattern. */} + + + + + + + + + {/* Game result text. */} + {text ? ( + + {text} + + ) : null} + + {/* Next / previous matchup lines. */} + {leftLeg} + {rightLeg} + + ); +}; diff --git a/website/src/components/common/FootballShape/index.styles.ts b/website/src/components/common/FootballShape/index.styles.ts deleted file mode 100644 index bb556610..00000000 --- a/website/src/components/common/FootballShape/index.styles.ts +++ /dev/null @@ -1,123 +0,0 @@ -import {darken} from 'polished'; -import styled from 'styled-components'; - -import {getColorForResult} from '../../../lib/utils'; -import {GameResult} from '../../../models'; - -interface FootballShapePathProps { - readonly $type: string; - readonly $gameResult: GameResult | null; - readonly $isSelected: boolean; -} - -export const FootballShapePath = styled.path` - cursor: pointer; - stroke-width: 2px; - stroke: ${({theme, $type, $gameResult, $isSelected}) => { - let color; - if ($isSelected) { - color = theme.colors.gold; - } else if ($type === 'past') { - color = $gameResult ? getColorForResult($gameResult) : 'black'; - } else { - color = theme.colors.black; - } - - return darken(0.2, color); - }}; - stroke-dasharray: ${({$type}) => ($type === 'future' ? '4px' : 0)}; - stroke-dashoffset: 3px; -`; - -interface FootballLacesPathProps { - readonly $isSelected: boolean; -} - -export const FootballLacesPath = styled.g` - cursor: pointer; - stroke-width: 2px; - stroke-linecap: round; - - line { - stroke: ${({theme, $isSelected}) => { - const color = $isSelected ? theme.colors.gold : theme.colors.black; - return darken(0.2, color); - }}; - } -`; - -interface FootballPatternProps { - readonly $type: string; - readonly $gameResult: GameResult | null; - readonly $isHomeGame: boolean; -} - -export const FootballPattern = styled.pattern` - rect { - fill: ${({$type, $gameResult}) => { - if ($type === 'past') { - return $gameResult ? getColorForResult($gameResult) : 'black'; - } else { - return 'transparent'; - } - }}; - } - - line { - stroke: ${({theme, $gameResult, $isHomeGame}) => { - if ($isHomeGame) { - return 'transparent'; - } - - return $gameResult - ? darken(0.2, getColorForResult($gameResult)) - : darken(0.3, theme.colors.lightGray); - }}; - stroke-width: 2px; - } -`; - -interface TextProps { - readonly $gameResult: GameResult | null; - readonly $isSelected: boolean; -} - -export const Text = styled.p` - cursor: pointer; - position: absolute; - top: calc(50% - 10px); - color: ${({theme, $gameResult, $isSelected}) => { - if ($isSelected) { - return $gameResult === GameResult.Tie ? darken(0.2, theme.colors.gold) : theme.colors.gold; - } else { - return theme.colors.white; - } - }}; - font-size: 14px; - font-family: 'Inter UI', serif; - width: 100%; -`; - -interface LegProps { - readonly $type: 'left' | 'right'; -} - -export const Leg = styled.div` - height: 0; - width: 24px; - border-bottom: solid 2px ${({theme}) => theme.colors.black}; - position: absolute; - bottom: -1; - left: ${({$type}) => ($type === 'left' ? 0 : 'initial')}; - right: ${({$type}) => ($type === 'right' ? 0 : 'initial')}; - transform: rotate(${({$type}) => ($type === 'right' ? 52 : -52)}deg); - - & > div { - margin-top: -4px; - margin-left: 8px; - width: 6px; - height: 10px; - border-left: solid 2px ${({theme}) => theme.colors.black}; - border-right: solid 2px ${({theme}) => theme.colors.black}; - } -`; diff --git a/website/src/components/common/FootballShape/index.tsx b/website/src/components/common/FootballShape/index.tsx deleted file mode 100644 index 419dc916..00000000 --- a/website/src/components/common/FootballShape/index.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; - -import {GameResult} from '../../../models'; -import {FootballLacesPath, FootballPattern, FootballShapePath, Leg, Text} from './index.styles'; - -export const FootballShape: React.FC<{ - readonly type: 'past' | 'future'; - readonly text?: string; - readonly title: string; - readonly gameResult: GameResult | null; - readonly legs?: { - readonly left?: string | boolean; - readonly right?: string | boolean; - }; - readonly isHomeGame: boolean; - readonly isSelected: boolean; - readonly uniqueFillPatternId: string; -}> = ({type, text, title, gameResult, legs = {}, isHomeGame, isSelected, uniqueFillPatternId}) => { - let leftLeg: React.ReactNode = null; - if (legs.left) { - leftLeg = {legs.left === 'gap' ?
 
: <> }
; - } - - let rightLeg: React.ReactNode = null; - if (legs.right) { - rightLeg = {legs.right === 'gap' ?
 
: <> }
; - } - - return ( - <> - - {title} - - {/* Football shape. */} - - - {/* Laces. */} - {type === 'future' ? ( - - - - - - - - - ) : null} - - {/* Fill pattern. */} - - - - - - - - - {/* Game result text. */} - {text ? ( - - {text} - - ) : null} - - {/* Next / previous matchup lines. */} - {leftLeg} - {rightLeg} - - ); -}; diff --git a/website/src/components/common/NewsletterSignupForm/index.styles.ts b/website/src/components/common/NewsletterSignupForm.tsx similarity index 52% rename from website/src/components/common/NewsletterSignupForm/index.styles.ts rename to website/src/components/common/NewsletterSignupForm.tsx index 66f0122b..c84d31ca 100644 --- a/website/src/components/common/NewsletterSignupForm/index.styles.ts +++ b/website/src/components/common/NewsletterSignupForm.tsx @@ -1,9 +1,10 @@ import {darken, lighten} from 'polished'; +import React, {useState} from 'react'; import styled from 'styled-components'; -import backgroundImage from '../../../images/background.png'; +import backgroundImage from '../../images/background.png'; -export const Wrapper = styled.div` +const Wrapper = styled.div` display: flex; margin: 20px auto; align-items: center; @@ -17,7 +18,7 @@ export const Wrapper = styled.div` } `; -export const Intro = styled.div` +const Intro = styled.div` width: 90%; max-width: 472px; text-align: center; @@ -41,16 +42,16 @@ export const Intro = styled.div` } `; -export const Form = styled.form` +const Form = styled.form` width: 100%; `; -export const HiddenBotInput = styled.input` +const HiddenBotInput = styled.input` position: absolute; left: -5000px; `; -export const FormInput = styled.input` +const FormInput = styled.input` display: block; width: 100%; max-width: 360px; @@ -79,7 +80,7 @@ export const FormInput = styled.input` } `; -export const SubscribeButton = styled.button` +const SubscribeButton = styled.button` display: block; width: 160px; height: 52px; @@ -100,3 +101,61 @@ export const SubscribeButton = styled.button` background-color: ${({theme}) => lighten(0.2, theme.colors.green)}; } `; + +export const NewsletterSignupForm: React.FC = () => { + const [email, setEmail] = useState(''); + const [firstName, setFirstName] = useState(''); + + const handleEmailChange = (event: React.ChangeEvent) => { + setEmail(event.target.value); + }; + + const handleFirstNameChange = (event: React.ChangeEvent) => { + setFirstName(event.target.value); + }; + + return ( + + +

Want more deep dives on Notre Dame football?

+

Subscribe to a low-volume newsletter to get notified when new content is published.

+
+ +
+ + + + + {/* From MailChimp: do not remove this or risk form bot signups */} + {}} + /> + + Subscribe + +
+ ); +}; diff --git a/website/src/components/common/NewsletterSignupForm/index.tsx b/website/src/components/common/NewsletterSignupForm/index.tsx deleted file mode 100644 index 5d9d064a..00000000 --- a/website/src/components/common/NewsletterSignupForm/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, {useState} from 'react'; - -import {Form, FormInput, HiddenBotInput, Intro, SubscribeButton, Wrapper} from './index.styles'; - -export const NewsletterSignupForm: React.FC = () => { - const [email, setEmail] = useState(''); - const [firstName, setFirstName] = useState(''); - - const handleEmailChange = (event: React.ChangeEvent) => { - setEmail(event.target.value); - }; - - const handleFirstNameChange = (event: React.ChangeEvent) => { - setFirstName(event.target.value); - }; - - return ( - - -

Want more deep dives on Notre Dame football?

-

Subscribe to a low-volume newsletter to get notified when new content is published.

-
- -
- - - - - {/* From MailChimp: do not remove this or risk form bot signups */} - {}} - /> - - Subscribe - -
- ); -}; diff --git a/website/src/components/common/SliderRange/index.tsx b/website/src/components/common/SliderRange.tsx similarity index 64% rename from website/src/components/common/SliderRange/index.tsx rename to website/src/components/common/SliderRange.tsx index ee5afeef..2051ca74 100644 --- a/website/src/components/common/SliderRange/index.tsx +++ b/website/src/components/common/SliderRange.tsx @@ -1,14 +1,55 @@ import {darken, lighten} from 'polished'; import Slider from 'rc-slider'; import React, {useState} from 'react'; +import styled from 'styled-components'; -import theme from '../../../resources/theme.json'; -import {SliderWrapper} from './index.styles'; +import theme from '../../resources/theme.json'; import 'rc-slider/assets/index.css'; +interface SliderWrapperProps { + readonly $width: number; + readonly $widthSm: number; +} + +const SliderWrapper = styled.div` + width: ${({$width}) => `${$width}px`}; + + @media (max-width: 600px) { + max-width: ${({$widthSm}) => `${$widthSm}px`}; + } + + .slider { + -webkit-appearance: none; + height: 15px; + border-radius: 5px; + background: ${({theme}) => darken(0.2, theme.colors.lightGray)}; + outline: none; + -webkit-transition: 0.2s; + transition: opacity 0.2s; + } + + .slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 25px; + height: 25px; + border-radius: 50%; + background: ${({theme}) => theme.colors.green}; + cursor: pointer; + } + + .slider::-moz-range-thumb { + width: 25px; + height: 25px; + border-radius: 50%; + background: ${({theme}) => theme.colors.green}; + cursor: pointer; + } +`; + const createSliderWithTooltip = Slider.createSliderWithTooltip; -export const Range = createSliderWithTooltip(Slider.Range); +const Range = createSliderWithTooltip(Slider.Range); interface SliderRangeProps { readonly min: number; diff --git a/website/src/components/common/SliderRange/index.styles.ts b/website/src/components/common/SliderRange/index.styles.ts deleted file mode 100644 index e67cc7ec..00000000 --- a/website/src/components/common/SliderRange/index.styles.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {darken} from 'polished'; -import styled from 'styled-components'; - -interface SliderWrapperProps { - readonly $width: number; - readonly $widthSm: number; -} - -export const SliderWrapper = styled.div` - width: ${({$width}) => `${$width}px`}; - - @media (max-width: 600px) { - max-width: ${({$widthSm}) => `${$widthSm}px`}; - } - - .slider { - -webkit-appearance: none; - height: 15px; - border-radius: 5px; - background: ${({theme}) => darken(0.2, theme.colors.lightGray)}; - outline: none; - -webkit-transition: 0.2s; - transition: opacity 0.2s; - } - - .slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 25px; - height: 25px; - border-radius: 50%; - background: ${({theme}) => theme.colors.green}; - cursor: pointer; - } - - .slider::-moz-range-thumb { - width: 25px; - height: 25px; - border-radius: 50%; - background: ${({theme}) => theme.colors.green}; - cursor: pointer; - } -`; diff --git a/website/src/components/common/StatsSection/index.styles.ts b/website/src/components/common/StatsSection.tsx similarity index 56% rename from website/src/components/common/StatsSection/index.styles.ts rename to website/src/components/common/StatsSection.tsx index 0d297481..fdfc1095 100644 --- a/website/src/components/common/StatsSection/index.styles.ts +++ b/website/src/components/common/StatsSection.tsx @@ -1,13 +1,14 @@ import {darken} from 'polished'; +import React from 'react'; import styled from 'styled-components'; -export const StatsSectionWrapper = styled.div` +const StatsSectionWrapper = styled.div` border: solid 3px ${({theme}) => theme.colors.black}; width: 100%; align-self: stretch; `; -export const StatsSectionTitle = styled.div` +const StatsSectionTitle = styled.div` text-align: center; margin: -11px 8px 24px 8px; @@ -25,9 +26,25 @@ export const StatsSectionTitle = styled.div` } `; -export const StatsChildrenWrapper = styled.div` +const StatsChildrenWrapper = styled.div` width: 100%; height: calc(100% - 12px); padding: 8px; margin-top: -20px; `; + +export const StatsSection: React.FC<{ + readonly title: string; + // TODO: Replace with `Spacer` component. + readonly style?: React.CSSProperties; + readonly children: React.ReactNode; +}> = ({title, style, children}) => { + return ( + + +

{title}

+
+ {children} +
+ ); +}; diff --git a/website/src/components/common/StatsSection/index.tsx b/website/src/components/common/StatsSection/index.tsx deleted file mode 100644 index 90b3d3f8..00000000 --- a/website/src/components/common/StatsSection/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -import {StatsChildrenWrapper, StatsSectionTitle, StatsSectionWrapper} from './index.styles'; - -export const StatsSection: React.FC<{ - readonly title: string; - // TODO: Replace with `Spacer` component. - readonly style?: React.CSSProperties; - readonly children: React.ReactNode; -}> = ({title, style, children}) => { - return ( - - -

{title}

-
- {children} -
- ); -}; diff --git a/website/src/components/common/YouTubeIcon/index.tsx b/website/src/components/common/YouTubeIcon.tsx similarity index 91% rename from website/src/components/common/YouTubeIcon/index.tsx rename to website/src/components/common/YouTubeIcon.tsx index 62d35223..aeb7ab0a 100644 --- a/website/src/components/common/YouTubeIcon/index.tsx +++ b/website/src/components/common/YouTubeIcon.tsx @@ -1,6 +1,29 @@ import React from 'react'; +import styled from 'styled-components'; -import {YouTubeIconSvg} from './index.styles'; +const RegularPaths = styled.g` + fill: ${({theme}) => theme.colors.black}; +`; + +const HoverPaths = styled.g` + fill: none; +`; + +const YouTubeIconSvg = styled.svg` + cursor: pointer; + width: 40px; + margin-top: 16px; + + &:hover { + ${RegularPaths} { + fill: none; + } + + ${HoverPaths} { + fill: ${({theme}) => theme.colors.green}; + } + } +`; export const YouTubeIcon: React.FC<{ readonly title: string; @@ -15,14 +38,14 @@ export const YouTubeIcon: React.FC<{ > {title} - + - - + + - + ); diff --git a/website/src/components/common/YouTubeIcon/index.styles.ts b/website/src/components/common/YouTubeIcon/index.styles.ts deleted file mode 100644 index b72916b9..00000000 --- a/website/src/components/common/YouTubeIcon/index.styles.ts +++ /dev/null @@ -1,25 +0,0 @@ -import styled from 'styled-components'; - -export const YouTubeIconSvg = styled.svg` - cursor: pointer; - width: 40px; - margin-top: 16px; - - .regular { - fill: ${({theme}) => theme.colors.black}; - } - - .hover { - fill: none; - } - - &:hover { - .regular { - fill: none; - } - - .hover { - fill: ${({theme}) => theme.colors.green}; - } - } -`;