From a2d5e2a0a6a7bf4975274435e34b02a825152ed5 Mon Sep 17 00:00:00 2001 From: Alex Xie Date: Wed, 23 Dec 2020 17:31:58 -0500 Subject: [PATCH] feat: add dark mode and full theming support (#81) --- .eslintrc | 3 +- README.md | 2 +- SHITLIST.md | 7 +- next.config.js | 14 +- src/components/Bio/About.tsx | 7 +- src/components/Bio/Work.tsx | 10 +- src/components/Bio/index.tsx | 6 +- src/components/CoverArt.tsx | 11 +- src/components/DynamicCurrentStatus.tsx | 4 +- src/components/DynamicFavicon.tsx | 11 +- src/components/DynamicTagline.tsx | 4 +- src/components/DynamicTime.tsx | 4 +- src/components/Footer.tsx | 32 +-- src/components/Heading.tsx | 24 -- src/components/MainIllustration/Me.tsx | 184 ++++++++++++++ src/components/MainIllustration/Tree.tsx | 195 +++++++++++++++ src/components/MainIllustration/index.tsx | 38 +++ .../layers.tsx | 0 src/components/MainIllustration/styles.tsx | 14 ++ src/components/MeIllustration/index.tsx | 236 ------------------ src/components/Title.tsx | 25 ++ src/components/core/Heading.tsx | 14 ++ src/components/core/Layout.tsx | 37 +++ src/components/core/Link.tsx | 49 ++-- src/components/core/Text.tsx | 15 +- src/components/core/index.ts | 2 + src/pages/404.tsx | 49 ++-- src/pages/index.tsx | 65 ++--- src/services/_server_/middleware.ts | 4 +- src/services/_server_/now-playing.ts | 5 +- src/services/_server_/storage.ts | 10 +- src/services/color.ts | 12 +- src/services/copy.ts | 8 +- src/services/github.ts | 7 +- src/services/now-playing.ts | 11 +- src/services/store/index.ts | 25 ++ src/services/store/{index.tsx => site.ts} | 28 +-- src/services/store/theme.ts | 79 ++++++ src/services/store/utils.ts | 8 +- src/services/{theme/index.ts => style.ts} | 22 +- src/services/utils.ts | 45 ++-- 41 files changed, 820 insertions(+), 506 deletions(-) delete mode 100644 src/components/Heading.tsx create mode 100644 src/components/MainIllustration/Me.tsx create mode 100644 src/components/MainIllustration/Tree.tsx create mode 100644 src/components/MainIllustration/index.tsx rename src/components/{MeIllustration => MainIllustration}/layers.tsx (100%) create mode 100644 src/components/MainIllustration/styles.tsx delete mode 100644 src/components/MeIllustration/index.tsx create mode 100644 src/components/Title.tsx create mode 100644 src/components/core/Heading.tsx create mode 100644 src/components/core/Layout.tsx create mode 100644 src/services/store/index.ts rename src/services/store/{index.tsx => site.ts} (80%) create mode 100644 src/services/store/theme.ts rename src/services/{theme/index.ts => style.ts} (70%) diff --git a/.eslintrc b/.eslintrc index 2eb1353b5..6731f9474 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,6 +10,7 @@ "plugins": ["prettier", "@typescript-eslint"], "rules": { "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-explicit-any": "off" + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off" } } diff --git a/README.md b/README.md index 0973d29a3..83848962e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# hey pal, here is the code that builds my website +# hi friend, here is the code that builds my website made with [preact](https://preactjs.com/), [next.js](https://nextjs.org/), [typescript](https://www.typescriptlang.org/), [goober](https://github.com/cristianbote/goober), [storeon](https://github.com/storeon/storeon), and a whole lotta googling. diff --git a/SHITLIST.md b/SHITLIST.md index eb16b0593..60ea1dfc9 100644 --- a/SHITLIST.md +++ b/SHITLIST.md @@ -2,8 +2,9 @@ still to do - [ ] visual regression testing - [x] 404 page -- [ ] themeing & dark mode -- [ ] easter egg -- [ ] accessibility (contrast/reduce motion) options +- [x] themeing & dark mode +- [x] easter egg +- [x] accessibility (contrast/reduce motion) options - [x] add api routes for custom status and location check-in - [x] add 'my work' page +- [x] move blog to craft diff --git a/next.config.js b/next.config.js index 482d27fde..54366e8a9 100644 --- a/next.config.js +++ b/next.config.js @@ -14,14 +14,12 @@ module.exports = withPreact({ } return config; }, - rewrites: async () => { - return [ - { - source: '/work', - destination: '/', - }, - ]; - }, + rewrites: async () => [ + { + source: '/work', + destination: '/', + }, + ], experimental: { modern: true, }, diff --git a/src/components/Bio/About.tsx b/src/components/Bio/About.tsx index d6199c5a8..9f4d2c1a1 100644 --- a/src/components/Bio/About.tsx +++ b/src/components/Bio/About.tsx @@ -1,16 +1,13 @@ import { FC } from 'react'; -import { useSiteStore } from 'services/store'; +import { useStore } from 'services/store'; import { Link, Text } from 'components/core'; import DynamicTime from 'components/DynamicTime'; import DynamicTagline from 'components/DynamicTagline'; import DynamicCurrentStatus from 'components/DynamicCurrentStatus'; const About: FC = () => { - const { talkingPoint, currentCity } = useSiteStore( - 'talkingPoint', - 'currentCity' - ); + const { talkingPoint, currentCity } = useStore('talkingPoint', 'currentCity'); return ( <> diff --git a/src/components/Bio/Work.tsx b/src/components/Bio/Work.tsx index cabd03de3..fe89ca033 100644 --- a/src/components/Bio/Work.tsx +++ b/src/components/Bio/Work.tsx @@ -2,10 +2,10 @@ import { FC, memo } from 'react'; import { Link, Text } from 'components/core'; import { PAST_EXPERIENCE } from 'services/copy'; -import { useSiteStore } from 'services/store'; +import { useStore } from 'services/store'; const Work: FC = memo(() => { - const { githubStats } = useSiteStore('githubStats'); + const { githubStats } = useStore('githubStats'); const latestRepo = githubStats?.reposCommittedTo[0] ?? null; return ( @@ -39,8 +39,10 @@ const Work: FC = memo(() => { return ( <> {isLast ? ' and ' : ' '} - - {label} + + + {label} + {isLast ? '.' : ','} diff --git a/src/components/Bio/index.tsx b/src/components/Bio/index.tsx index 0bb09cca1..5c7975506 100644 --- a/src/components/Bio/index.tsx +++ b/src/components/Bio/index.tsx @@ -1,8 +1,8 @@ import { FC, useEffect } from 'react'; import { styled } from 'goober'; -import { useSiteStore } from 'services/store'; -import { screen } from 'services/utils'; +import { useStore } from 'services/store'; +import { screen } from 'services/style'; import About from './About'; import Work from './Work'; @@ -31,7 +31,7 @@ const Subcontainer = styled('div')` `; const Bio: FC = () => { - const { dispatch, displayedSection } = useSiteStore('displayedSection'); + const { dispatch, displayedSection } = useStore('displayedSection'); useEffect(() => { if (process.browser && window.location.pathname === '/work') { diff --git a/src/components/CoverArt.tsx b/src/components/CoverArt.tsx index 74f5f316c..0d57b5e3e 100644 --- a/src/components/CoverArt.tsx +++ b/src/components/CoverArt.tsx @@ -2,18 +2,13 @@ import { css, keyframes } from 'goober'; import { FC, memo } from 'react'; import { useStoreFocusListeners } from 'services/store/utils'; -import { screen } from 'services/utils'; +import { screen } from 'services/style'; type TCoverArtProps = { link: string; src: string; color: string }; const rotate = keyframes` - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } `; const CoverArtLink = css` diff --git a/src/components/DynamicCurrentStatus.tsx b/src/components/DynamicCurrentStatus.tsx index d31baf04b..cc6c7d60d 100644 --- a/src/components/DynamicCurrentStatus.tsx +++ b/src/components/DynamicCurrentStatus.tsx @@ -1,7 +1,7 @@ import { memo, FC, useCallback, useEffect } from 'react'; import TextLoop from 'react-text-loop'; -import { useSiteStore } from 'services/store'; +import { useStore } from 'services/store'; import { TNowPlayingData, isNowPlayingData } from 'services/now-playing'; import { textLoopIntervals, @@ -45,7 +45,7 @@ const nowPlayingMarkup = ({ }; const DynamicCurrentStatus: FC = memo(() => { - const { dispatch, statuses } = useSiteStore('statuses'); + const { dispatch, statuses } = useStore('statuses'); const statusesMarkup = statuses.map((status) => isNowPlayingData(status) ? nowPlayingMarkup(status) : status.split(' ') ); diff --git a/src/components/DynamicFavicon.tsx b/src/components/DynamicFavicon.tsx index 1de65ce1b..de050bafe 100644 --- a/src/components/DynamicFavicon.tsx +++ b/src/components/DynamicFavicon.tsx @@ -3,17 +3,16 @@ import { useState, FC } from 'react'; import { useVisibilityChange } from 'services/utils'; -const DynamicFavicon: FC = () => { +const DynamicFavicon: FC<{ face?: 'smile' | 'mad' }> = ({ face }) => { const [isAway, setAway] = useState(false); useVisibilityChange(setAway); + const dynamicFace = face ?? isAway ? 'mad' : 'smile'; + const href = dynamicFace === 'mad' ? '/favicon-away.png' : '/favicon.png'; + return ( - + ); }; diff --git a/src/components/DynamicTagline.tsx b/src/components/DynamicTagline.tsx index fc9974baa..69542cddf 100644 --- a/src/components/DynamicTagline.tsx +++ b/src/components/DynamicTagline.tsx @@ -1,10 +1,10 @@ import { memo, FC } from 'react'; import TextLoop from 'react-text-loop'; -import { useSiteStore } from 'services/store'; +import { useStore } from 'services/store'; const DynamicTagline: FC = memo(() => { - const { taglines } = useSiteStore('taglines'); + const { taglines } = useStore('taglines'); return ; }); diff --git a/src/components/DynamicTime.tsx b/src/components/DynamicTime.tsx index e0fc4a3ba..e0fa4e23c 100644 --- a/src/components/DynamicTime.tsx +++ b/src/components/DynamicTime.tsx @@ -1,7 +1,7 @@ import { FC, useState, useEffect } from 'react'; import TextLoop from 'react-text-loop'; -import { useSiteStore } from 'services/store'; +import { useStore } from 'services/store'; import { textLoopIntervals } from 'services/utils'; import GradientText from 'components/GradientText'; @@ -71,7 +71,7 @@ const timeToGradient = (hour: number, time: string): TextGradientInfo => { }; const DynamicTime: FC = () => { - const { currentDate } = useSiteStore('currentDate'); + const { currentDate } = useStore('currentDate'); const [dates, setDates] = useState([currentDate]); useEffect(() => { diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index ae11857bd..73ec0ad6f 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -3,7 +3,8 @@ import { styled } from 'goober'; import { LINKS } from 'services/copy'; import { Link, Text } from 'components/core'; -import { useSiteStore } from 'services/store'; +import { useStore } from 'services/store'; +import { onClickListeners } from 'services/utils'; const Container = styled('footer')` display: flex; @@ -13,28 +14,31 @@ const Container = styled('footer')` margin: 0 0 2em 0; - & > a { + & > * { margin: 0 6px; } `; const Footer: FC = memo(() => { - const { dispatch, displayedSection } = useSiteStore('displayedSection'); + const { dispatch, displayedSection } = useStore('displayedSection'); return ( {LINKS.map(({ label, href }) => ( - - {label} - + + + {label} + + ))} - dispatch('section/toggle')} - onKeyUp={(e) => (e.key === 'Enter' ? dispatch('section/toggle') : null)} - role="button" - tabIndex={0} - > - {displayedSection === 'about' ? 'my work' : 'about me'} - + + dispatch('section/toggle'))} + role="button" + tabIndex={0} + > + {displayedSection === 'about' ? 'my work' : 'about me'} + + ); }); diff --git a/src/components/Heading.tsx b/src/components/Heading.tsx deleted file mode 100644 index b9f05487d..000000000 --- a/src/components/Heading.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { memo, FC } from 'react'; -import { styled } from 'goober'; - -import { useSiteStore } from 'services/store'; -import { screen } from 'services/utils'; - -const H1 = styled('h1')` - font-family: 'Verona Serial', 'Franklin Gothic Medium', Arial, serif; - font-size: 48px; - text-align: center; - margin: 48px 0; - - ${screen.mobile} { - font-size: 40px; - margin: 24px 0 28px 0; - } -`; - -const Heading: FC = memo(() => { - const { greeting } = useSiteStore('greeting'); - return

{greeting}

; -}); - -export default Heading; diff --git a/src/components/MainIllustration/Me.tsx b/src/components/MainIllustration/Me.tsx new file mode 100644 index 000000000..031ae9328 --- /dev/null +++ b/src/components/MainIllustration/Me.tsx @@ -0,0 +1,184 @@ +import { FC, memo } from 'react'; + +import { useStore } from 'services/store'; +import { onClickListeners, useHoverListeners } from 'services/utils'; + +import Layers from './layers'; +import { Group } from './styles'; + +const SantaHat = memo(() => Layers.SANTA_HAT); + +const MeIllustration: FC = () => { + const { dispatch, isFocusingOnSomething, isDarkMode } = useStore( + 'isFocusingOnSomething', + 'isDarkMode' + ); + const { + isHovering, + setHovering, + listeners: hoverListeners, + } = useHoverListeners(); + + const expression = isFocusingOnSomething + ? Layers.SURPRISED + : isHovering + ? Layers.WEIRD + : Layers.GRIN_HAPPY; + + const onIllustrationClick = () => { + setHovering(false); + dispatch('easter-egg/toggle'); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {expression} + + + + + + + ); +}; + +export default MeIllustration; diff --git a/src/components/MainIllustration/Tree.tsx b/src/components/MainIllustration/Tree.tsx new file mode 100644 index 000000000..cd9d00112 --- /dev/null +++ b/src/components/MainIllustration/Tree.tsx @@ -0,0 +1,195 @@ +import { keyframes } from 'goober'; + +import { useStore } from 'services/store'; +import { onClickListeners, useHoverListeners } from 'services/utils'; + +import { Group } from './styles'; + +const LIT_STYLES = { + fill: '#FFC81A', +}; +const DIM_STYLES = { fill: '#eee' }; + +const blink = keyframes` + from { fill: #FFC81A; opacity: 0.5; } + 50% { fill: #FFC81A; opacity: 1; } + to { fill: #FFC81A; opacity: 0.5; } +`; + +const BLINKING_LIGHTS = [ + { + d: + 'M180.713049,99.0088838 C180.713049,112.578696 169.713267,123.578478 156.143455,123.578478 C142.573643,123.578478 131.573861,112.578696 131.573861,99.0088838 C131.573861,85.4390715 142.573643,74.4392895 156.143455,74.4392895 C169.713267,74.4392895 180.713049,85.4390715 180.713049,99.0088838', + transform: + 'translate(156.143455, 99.008884) scale(-1, 1) translate(-156.143455, -99.008884)', + }, + { + d: + 'M399.174813,307.97782 C399.174813,321.547632 388.175031,332.547414 374.605219,332.547414 C361.035407,332.547414 350.035625,321.547632 350.035625,307.97782 C350.035625,294.408008 361.035407,283.408226 374.605219,283.408226 C388.175031,283.408226 399.174813,294.408008 399.174813,307.97782', + transform: + 'translate(374.605219, 307.977820) scale(-1, 1) translate(-374.605219, -307.977820)', + }, + { + d: + 'M107.002953,490.391266 C107.002953,503.961078 96.0031714,514.96086 82.4333591,514.96086 C68.8635469,514.96086 57.8637649,503.961078 57.8637649,490.391266 C57.8637649,476.821453 68.8635469,465.821671 82.4333591,465.821671 C96.0031714,465.821671 107.002953,476.821453 107.002953,490.391266', + transform: + 'translate(82.433359, 490.391266) scale(-1, 1) translate(-82.433359, -490.391266)', + }, + { + d: + 'M448.314659,496.603595 C448.314659,510.173407 437.314877,521.173189 423.745064,521.173189 C410.175252,521.173189 399.17547,510.173407 399.17547,496.603595 C399.17547,483.033782 410.175252,472.034 423.745064,472.034 C437.314877,472.034 448.314659,483.033782 448.314659,496.603595', + transform: + 'translate(423.745064, 496.603595) scale(-1, 1) translate(-423.745064, -496.603595)', + }, + { + d: + 'M150.279269,291.166513 C150.279269,304.736325 139.279487,315.736107 125.709674,315.736107 C112.139862,315.736107 101.14008,304.736325 101.14008,291.166513 C101.14008,277.596701 112.139862,266.596919 125.709674,266.596919 C139.279487,266.596919 150.279269,277.596701 150.279269,291.166513', + transform: + 'translate(125.709674, 291.166513) scale(-1, 1) translate(-125.709674, -291.166513)', + }, + { + d: + 'M293.413275,577.354418 C293.413275,590.92423 282.413493,601.924012 268.843681,601.924012 C255.273869,601.924012 244.274087,590.92423 244.274087,577.354418 C244.274087,563.784606 255.273869,552.784824 268.843681,552.784824 C282.413493,552.784824 293.413275,563.784606 293.413275,577.354418', + transform: + 'translate(268.843681, 577.354418) scale(-1, 1) translate(-268.843681, -577.354418)', + }, + { + d: + 'M49.1391885,714.817286 C49.1391885,728.387098 38.1394065,739.388193 24.5695943,739.388193 C10.999782,739.388193 3.63797881e-12,728.387098 3.63797881e-12,714.817286 C3.63797881e-12,701.248787 10.999782,690.247692 24.5695943,690.247692 C38.1394065,690.247692 49.1391885,701.248787 49.1391885,714.817286', + transform: + 'translate(24.569594, 714.817942) scale(-1, 1) translate(-24.569594, -714.817942)', + }, + { + d: + 'M310.464849,24.5695943 C310.464849,38.1394065 299.465067,49.1391885 285.895254,49.1391885 C272.325442,49.1391885 261.32566,38.1394065 261.32566,24.5695943 C261.32566,10.999782 272.325442,6.60804744e-13 285.895254,6.60804744e-13 C299.465067,6.60804744e-13 310.464849,10.999782 310.464849,24.5695943', + transform: + 'translate(285.895254, 24.569594) scale(-1, 1) translate(-285.895254, -24.569594)', + }, + { + d: + 'M244.273561,479.028633 C244.273561,492.598445 233.273779,503.598227 219.703967,503.598227 C206.134155,503.598227 195.134373,492.598445 195.134373,479.028633 C195.134373,465.45882 206.134155,454.459038 219.703967,454.459038 C233.273779,454.459038 244.273561,465.45882 244.273561,479.028633', + transform: + 'translate(219.703967, 479.028633) scale(-1, 1) translate(-219.703967, -479.028633)', + }, + { + d: + 'M293.413275,243.378527 C293.413275,256.94834 282.413493,267.948122 268.843681,267.948122 C255.273869,267.948122 244.274087,256.94834 244.274087,243.378527 C244.274087,229.808715 255.273869,218.808933 268.843681,218.808933 C282.413493,218.808933 293.413275,229.808715 293.413275,243.378527', + transform: + 'translate(268.843681, 243.378527) scale(-1, 1) translate(-268.843681, -243.378527)', + }, + { + d: + 'M448.314659,723.273828 C448.314659,736.84364 437.314877,747.843422 423.745064,747.843422 C410.175252,747.843422 399.17547,736.84364 399.17547,723.273828 C399.17547,709.704016 410.175252,698.704234 423.745064,698.704234 C437.314877,698.704234 448.314659,709.704016 448.314659,723.273828', + transform: + 'translate(423.745064, 723.273828) scale(-1, 1) translate(-423.745064, -723.273828)', + }, + { + d: + 'M244.273561,754.686768 C244.273561,768.25658 233.273779,779.256362 219.703967,779.256362 C206.134155,779.256362 195.134373,768.25658 195.134373,754.686768 C195.134373,741.116956 206.134155,730.117174 219.703967,730.117174 C233.273779,730.117174 244.273561,741.116956 244.273561,754.686768', + transform: + 'translate(219.703967, 754.686768) scale(-1, 1) translate(-219.703967, -754.686768)', + }, +].map((props, i) => ( + +)); + +const Tree = () => { + const { dispatch, isDarkMode } = useStore('isDarkMode'); + const { + isHovering, + setHovering, + listeners: hoverListeners, + } = useHoverListeners(); + + const onIllustrationClick = () => { + setHovering(false); + dispatch('dark-mode/toggle', undefined); + }; + + const starStyles = + (isDarkMode && !isHovering) || (!isDarkMode && isHovering) + ? LIT_STYLES + : DIM_STYLES; + + return ( + + + + + + + + + + + + + + + {BLINKING_LIGHTS} + + + + + + + + ); +}; + +export default Tree; diff --git a/src/components/MainIllustration/index.tsx b/src/components/MainIllustration/index.tsx new file mode 100644 index 000000000..c3640d0fc --- /dev/null +++ b/src/components/MainIllustration/index.tsx @@ -0,0 +1,38 @@ +import { styled } from 'goober'; + +import { screen } from 'services/style'; + +import Me from './Me'; +import Tree from './Tree'; + +const Container = styled('div')` + position: relative; + display: flex; + + & #illustration-me { + height: 200px; + transform: translate(-50px, 100px); + } + + & #illustration-tree { + height: 300px; + } + + ${screen.mobile} { + & #illustration-me { + transform: translate(-50px, 120px); + height: 180px; + } + } +`; + +const MainIllustration = () => { + return ( + + + + + ); +}; + +export default MainIllustration; diff --git a/src/components/MeIllustration/layers.tsx b/src/components/MainIllustration/layers.tsx similarity index 100% rename from src/components/MeIllustration/layers.tsx rename to src/components/MainIllustration/layers.tsx diff --git a/src/components/MainIllustration/styles.tsx b/src/components/MainIllustration/styles.tsx new file mode 100644 index 000000000..5d3438c04 --- /dev/null +++ b/src/components/MainIllustration/styles.tsx @@ -0,0 +1,14 @@ +import { styled } from 'goober'; + +export const Group = styled('g')` + cursor: pointer; + stroke-width: 1; + + &:focus { + outline: none; + } + + &:focus-visible { + outline: 1px solid blue; + } +`; diff --git a/src/components/MeIllustration/index.tsx b/src/components/MeIllustration/index.tsx deleted file mode 100644 index 8433abb5e..000000000 --- a/src/components/MeIllustration/index.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { FC, memo, useState } from 'react'; -import { styled } from 'goober'; - -import { useSiteStore } from 'services/store'; -import { screen } from 'services/utils'; - -import Layers from './layers'; - -const Container = styled('svg')` - height: 280px; - - ${screen.mobile} { - height: 180px; - } -`; - -const SantaHat = memo(() => Layers.SANTA_HAT); - -const MeIllustration: FC = () => { - const { isFocusingOnSomething } = useSiteStore('isFocusingOnSomething'); - const [, setNumClicks] = useState(0); - const [isHovering, setHovering] = useState(false); - const expression = isFocusingOnSomething - ? Layers.SURPRISED - : isHovering - ? Layers.WEIRD - : Layers.GRIN_HAPPY; - - const onIllustrationClick = () => - setNumClicks((prev) => { - if ((prev + 1) % 3 === 0) - window.open('https://www.youtube.com/watch?v=dQw4w9WgXcQ', '_blank'); - return prev + 1; - }); - - return ( - setHovering(true)} - onMouseLeave={() => setHovering(false)} - onClick={onIllustrationClick} - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {expression} - - - - - - - - - - ); -}; - -export default MeIllustration; diff --git a/src/components/Title.tsx b/src/components/Title.tsx new file mode 100644 index 000000000..5c816646f --- /dev/null +++ b/src/components/Title.tsx @@ -0,0 +1,25 @@ +import { memo, FC } from 'react'; + +import { useStore } from 'services/store'; +import { s } from 'services/style'; +import { H1 } from 'components/core'; + +const Container = s('header')` + & > h1.eeActive { + font-family: ${({ theme }) => theme!.easterEggFont} !important; + } +`; + +const Title: FC = memo(() => { + const { greeting, isEasterEggActive } = useStore( + 'greeting', + 'isEasterEggActive' + ); + return ( + +

{greeting}

+
+ ); +}); + +export default Title; diff --git a/src/components/core/Heading.tsx b/src/components/core/Heading.tsx new file mode 100644 index 000000000..b695c53bb --- /dev/null +++ b/src/components/core/Heading.tsx @@ -0,0 +1,14 @@ +import { s, screen } from 'services/style'; + +export const H1 = s('h1')` + font-family: ${({ theme }) => theme!.headingFont}; + color: ${({ theme }) => theme!.colors.textPrimary}; + font-size: 48px; + text-align: center; + margin: 48px 0; + + ${screen.mobile} { + font-size: 40px; + margin: 24px 0 28px 0; + } +`; diff --git a/src/components/core/Layout.tsx b/src/components/core/Layout.tsx new file mode 100644 index 000000000..ac4ed1581 --- /dev/null +++ b/src/components/core/Layout.tsx @@ -0,0 +1,37 @@ +import { s, screen } from 'services/style'; + +export const AppContainer = s('div')` + position: relative; + width: 100vw; + height: 100vh; + background-color: ${({ theme }) => theme!.colors.background}; + transition: background-color 400ms; +`; + +export const ContentContainer = s('div')` + position: relative; + width: 100vw; + min-height: 100vh; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + ${screen.mobile} { + min-height: unset; + } +`; + +export const InnerContentContainer = s('main')` + position: relative; + width: 100%; + height: 100%; + width: 80vw; + max-width: 510px; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +`; diff --git a/src/components/core/Link.tsx b/src/components/core/Link.tsx index 618946b20..715e346a7 100644 --- a/src/components/core/Link.tsx +++ b/src/components/core/Link.tsx @@ -1,8 +1,8 @@ import { styled } from 'goober'; -import { FC } from 'react'; +import { FC, memo } from 'react'; import { useStoreFocusListeners } from 'services/store/utils'; -import { screen } from 'services/utils'; +import { screen } from 'services/style'; type LinkProps = React.ComponentPropsWithoutRef<'a'> & { bare?: boolean; @@ -10,18 +10,14 @@ type LinkProps = React.ComponentPropsWithoutRef<'a'> & { }; const A = styled('a')` - font-size: 16px; - font-family: 'Space Grotesk Variable', 'Space Grotesk', -apple-system, - BlinkMacSystemFont, Roboto, Ubuntu, 'Helvetica Neue', sans-serif; color: inherit; - cursor: pointer; transition: opacity 250ms; text-decoration: ${({ bare }) => (bare ? 'none' : 'underline')}; &:hover { - text-decoration: underline dotted; - opacity: ${({ bare }) => (bare ? 0.8 : 1)}; + text-decoration: none; + opacity: ${({ bare }) => (bare ? 0.65 : 1)}; } &:focus { @@ -37,24 +33,19 @@ const A = styled('a')` } `; -const Link: FC = ({ - bare = false, - newTab = false, - children, - ...rest -}) => { - const focusListeners = useStoreFocusListeners(); - - return ( - - {children as any} - - ); -}; - -export { Link }; +export const Link: FC = memo( + ({ bare = false, newTab = false, children, ...rest }) => { + const focusListeners = useStoreFocusListeners(); + + return ( + + {children as any} + + ); + } +); diff --git a/src/components/core/Text.tsx b/src/components/core/Text.tsx index dd2a23560..f08777366 100644 --- a/src/components/core/Text.tsx +++ b/src/components/core/Text.tsx @@ -1,18 +1,23 @@ -import { styled } from 'goober'; import { ComponentPropsWithoutRef } from 'react'; -import { screen } from 'services/utils'; + +import { TThemeColor } from 'services/store/theme'; +import { s, screen } from 'services/style'; // the `as` prop isn't typed by Goober for some reason, but it exists (https://github.com/cristianbote/goober#using-as-prop) type TTextProps = { as?: keyof JSX.IntrinsicElements; bold?: boolean; italic?: boolean; + color?: string; } & ComponentPropsWithoutRef<'span'>; -export const Text = styled('span')` +export const Text = s('span')` + color: ${({ theme, color }) => + color + ? theme!.colors[color as TThemeColor] ?? color + : theme!.colors.textPrimary}; + font-family: ${({ theme }) => theme!.bodyFont}; font-size: 16px; - font-family: 'Space Grotesk Variable', 'Space Grotesk', -apple-system, - BlinkMacSystemFont, Roboto, Ubuntu, 'Helvetica Neue', sans-serif; ${({ bold }) => (bold ? `font-weight: 500;` : '')} ${({ italic }) => (italic ? `font-style: italic;` : '')} diff --git a/src/components/core/index.ts b/src/components/core/index.ts index 64e989492..3bb423a01 100644 --- a/src/components/core/index.ts +++ b/src/components/core/index.ts @@ -1,2 +1,4 @@ +export * from './Heading'; +export * from './Layout'; export { Text } from './Text'; export { Link } from './Link'; diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 281dafe3e..e1913552c 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,20 +1,11 @@ import { styled } from 'goober'; import Head from 'next/head'; +import { StoreContext } from 'storeon/preact'; -import 'services/theme'; +import 'services/style'; +import { createThemeStore } from 'services/store'; import DynamicFavicon from 'components/DynamicFavicon'; -import { Text } from 'components/core'; - -const AppContainer = styled('div')` - position: relative; - width: 100vw; - min-height: 100vh; - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -`; +import { Text, AppContainer, ContentContainer } from 'components/core'; const NotFoundImg = styled('img')` margin-top: 1em; @@ -22,18 +13,22 @@ const NotFoundImg = styled('img')` max-width: 80vw; `; -const NotFoundPage = () => ( - <> - - There's nothing here. - - - - - Seems like you're a bit lost. - - - -); +export default function NotFoundPage() { + return ( + <> + + There's nothing here. + + -export default NotFoundPage; + + + + Seems like you're a bit lost. + + + + + + ); +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ceae05696..353886628 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,56 +1,30 @@ -import { styled } from 'goober'; import { InferGetStaticPropsType } from 'next'; import Head from 'next/head'; import dynamic from 'next/dynamic'; +import { StoreContext } from 'storeon/preact'; -import 'services/theme'; import { getNowPlayingDataServerSide, StorageClient, StorageKey, } from 'services/_server_'; import { createSiteStore } from 'services/store'; +import { getGithubStats } from 'services/github'; import DynamicFavicon from 'components/DynamicFavicon'; -import Heading from 'components/Heading'; +import Title from 'components/Title'; import Bio from 'components/Bio'; import Footer from 'components/Footer'; -import { StoreContext } from 'storeon/preact'; -import { getGithubStats } from 'services/github'; -import { screen } from 'services/utils'; - -type TPageInitialProps = InferGetStaticPropsType; - -const MeIllustration = dynamic(() => import('components/MeIllustration')); - -const AppContainer = styled('div')` - position: relative; - width: 100vw; - min-height: 100vh; - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - ${screen.mobile} { - min-height: unset; - } -`; +import { + AppContainer, + ContentContainer, + InnerContentContainer, +} from 'components/core'; -const ContentContainer = styled('main')` - position: relative; - width: 100%; - height: 100%; - width: 80vw; - max-width: 510px; +export type TPageInitialProps = InferGetStaticPropsType; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -`; +const MainIllustration = dynamic(() => import('components/MainIllustration')); -const IndexPage = (initialProps: TPageInitialProps) => { +export default function IndexPage(initialProps: TPageInitialProps) { return ( <> @@ -72,16 +46,18 @@ const IndexPage = (initialProps: TPageInitialProps) => { - - - -