From 0a3b7e8b3bbb55553527e21f2bd0391e5a80fb13 Mon Sep 17 00:00:00 2001 From: Alex Tio <85657217+alextio@users.noreply.github.com> Date: Mon, 10 Jun 2024 19:43:45 +0900 Subject: [PATCH] Feat/responsive design (#21) --- public/images/Hamburger-icon.svg | 1 + public/images/hamburger-icon.png | Bin 0 -> 283 bytes src/app/GlobalStyles.tsx | 3 + src/app/homepage/HeroSection.tsx | 123 +++++++++------ src/app/homepage/MediaSection.tsx | 62 ++++---- src/app/homepage/ResearchThemesSection.tsx | 6 +- src/app/homepage/Styles.tsx | 10 +- src/app/theme.ts | 25 +++ src/components/Filter.tsx | 4 +- src/components/Footer.tsx | 1 + src/components/NavBar.tsx | 167 +++++++++++++++++---- src/components/Section.tsx | 19 ++- src/data/videos.ts | 36 +++++ 13 files changed, 345 insertions(+), 112 deletions(-) create mode 100644 public/images/Hamburger-icon.svg create mode 100644 public/images/hamburger-icon.png create mode 100644 src/data/videos.ts diff --git a/public/images/Hamburger-icon.svg b/public/images/Hamburger-icon.svg new file mode 100644 index 0000000..233bf21 --- /dev/null +++ b/public/images/Hamburger-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/hamburger-icon.png b/public/images/hamburger-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b3f0673a8ea5d38c3de5367ff06e43f91a641617 GIT binary patch literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^3P3E!!3HFKxnwkf6lZ})WHAE+w=f7ZGR&GI0Tg5` z4sv&5Sa(k5C6L3C?&#~tz_78O`%fY(kiWsx#WAFU@$EH7-opkwtO2*R8k{s*f+Sju zZm!#DvipghN^<~Lvj7L9fa>piTO2#8KTMwFwRgwHHGhgY5>uZo{%&_ULNZ%8$G`be z)EtlOfc`p0wb;Sxw&ej*Bv^AHCTRvY?`>s*qaZGk>=7P*wMz7M2scv~~ zK7ldig74-BUTy)^;+sNUZadu1k>gsfA2qvP-#~zycL}q{xt+!a|24lFSh-5Xp3vy| baO0lkDmTt2XF6(tZfEdx^>bP0l+XkK`95TO literal 0 HcmV?d00001 diff --git a/src/app/GlobalStyles.tsx b/src/app/GlobalStyles.tsx index 7c55e18..d132491 100644 --- a/src/app/GlobalStyles.tsx +++ b/src/app/GlobalStyles.tsx @@ -25,6 +25,9 @@ export default function GlobalStyles() { /* https://www.w3.org/WAI/WCAG21/Understanding/text-spacing.html*/ body { line-height: 1.5; + display: flex; + flex-direction: column; + min-height: 100vh; } /* Prevent media from overflowing and make them not inline elements */ diff --git a/src/app/homepage/HeroSection.tsx b/src/app/homepage/HeroSection.tsx index 6197a81..8c3b448 100644 --- a/src/app/homepage/HeroSection.tsx +++ b/src/app/homepage/HeroSection.tsx @@ -1,57 +1,90 @@ 'use client' import Image from 'next/image' -import { Color } from '@/app/theme' +import { Color, ScreenSize, linearlyScaleSize } from '@/app/theme' import styled from '@emotion/styled' import React from 'react' import { Section } from './Styles' -const RespFontSize = { - title_xl: '2.25rem', - title_lg: '1.75rem', - title_sm: '1rem', -} as const +export const HeroSection = () => { + const RespFontSize = { + title_xl: '2.25rem', + title_lg: '1.75rem', + title_sm: '1rem', + } as const -const HeroContainer = styled.div` - display: flex; -` + const HeroContainer = styled.div` + display: flex; + position: relative; + justify-content: space-between; + // TODO: Decide whether to keep gap + // gap: 24px; -const HeroTextArea = styled.div` - flex-basis: 50%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: start; - padding: 90px 96px; -` + @media (max-width: ${ScreenSize.md}) { + flex-direction: column; + gap: 0px; + } + ` -const HeroTitle = styled.h1` - font-size: clamp(2rem, 1vw + 2rem, 3.5rem); - font-weight: 700; - letter-spacing: 0.5px; - margin-bottom: 24px; -` -const HeroSubtitle = styled.h2` - font-size: clamp(1.25rem, 0.3125 + 4.17vw, 2.5rem); - font-weight: 300; - margin-bottom: 32px; -` -const HeroMessage = styled.p` - font-size: clamp(1rem, 0.65vw + 0.5rem, 2rem); - color: ${Color.gray700}; - text-align: justify; - min-width: 300px; - max-width: 100%; -` + const HeroTextArea = styled.div` + flex-basis: 50%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: start; + padding: ${linearlyScaleSize({ + minSizePx: 24, + maxSizePx: 96, + minScreenSizePx: parseInt(ScreenSize.md), + maxScreenSizePx: parseInt(ScreenSize.lg), + })} + ${linearlyScaleSize({ + minSizePx: 48, + maxSizePx: 96, + minScreenSizePx: parseInt(ScreenSize.md), + maxScreenSizePx: parseInt(ScreenSize.lg), + })}; + ` -const HeroImageContainer = styled.div` - flex-basis: 50%; - display: grid; - align-content: center; - position: relative; - z-index: 0; -` + const HeroTitle = styled.h1` + // TODO: Try to make the responsive font-size more systematic. This is a temporary fix. + font-size: clamp(2rem, 1vw + 2rem, 3.5rem); + font-weight: 700; + letter-spacing: 0.5px; + margin-bottom: 24px; + text-align: left; -export const HeroSection = () => { + @media (max-width: ${ScreenSize.md}) { + align-self: center; + text-align: center; + } + ` + + const HeroSubtitle = styled.h2` + font-size: clamp(1.25rem, 0.3125 + 4.17vw, 2.5rem); + font-weight: 300; + margin-bottom: 32px; + + @media (max-width: ${ScreenSize.md}) { + align-self: center; + text-align: center; + } + ` + const HeroMessage = styled.p` + font-size: clamp(1rem, 0.65vw + 0.5rem, 2rem); + color: ${Color.gray700}; + text-align: justify; + max-width: 100%; + ` + + const HeroImageContainer = styled.div` + flex-basis: 50%; + position: relative; + z-index: 0; + @media (max-width: ${ScreenSize.md}) { + // TODO: Set the min-height more systematically. This is a temporary fix. + min-height: 30vh; + } + ` return (
{/* padding: 0 is to allow image to stretch to the right side of the webpage*/} @@ -69,14 +102,14 @@ export const HeroSection = () => { online by designing new interactive systems that leverage and support interaction at scale. - + Kixlab group picture diff --git a/src/app/homepage/MediaSection.tsx b/src/app/homepage/MediaSection.tsx index bb01bd7..e121620 100644 --- a/src/app/homepage/MediaSection.tsx +++ b/src/app/homepage/MediaSection.tsx @@ -3,7 +3,8 @@ import { FontVariant } from '@/app/theme' import styled from '@emotion/styled' import { SectionHeader } from '@/components/Section' import React from 'react' -import { Section, GridContainer } from './Styles' +import { Section } from './Styles' +import VIDEOS from '@/data/videos' const MediaArea = styled.div` display: flex; @@ -11,50 +12,53 @@ const MediaArea = styled.div` flex: auto; flex-wrap: wrap; justify-content: space-between; - gap: 72px; + gap: 32px; ` + +const VideoCard = styled.div` + flex: 1 1 0; + display: flex; + flex-direction: column; + gap: 4px; + justify-content: normal; +` +const VideoContainer = styled.div` + max-height: 300px; + aspect-ratio: 16 / 9; +` + const VideoTitle = styled.h3` ${FontVariant.title_md} text-align: center; ` + const VideoDate = styled.h4` text-align: center; font-size: 16px; ` -const videos = [ - { - url: 'https://www.youtube.com/embed/j0v1Cr74kN8?si=tp_blkpKqN4FP9te', - title: 'What is Interaction-centric AI?', - date: '2022.10.28', - }, - { - url: 'https://www.youtube.com/embed/pkhTuiYvvw4?si=hUee7IqJ-m95L1k2', - title: 'KIXLAB Introduction', - date: '2021.02.12', - }, - { url: 'https://www.youtube.com/embed/GgvkmXXPFPI?si=YWMLcLMhac5kRYzJ', title: 'Open KAIST', date: '2022.01.10' }, -] export const MediaSection = () => { return (
- {videos.map((video, i) => ( - - + {VIDEOS.map(video => ( + + + + {video.title} - {video.date} - + {video.formattedDate} + ))}
diff --git a/src/app/homepage/ResearchThemesSection.tsx b/src/app/homepage/ResearchThemesSection.tsx index aa9d8d4..4ebbbdd 100644 --- a/src/app/homepage/ResearchThemesSection.tsx +++ b/src/app/homepage/ResearchThemesSection.tsx @@ -1,6 +1,6 @@ 'use client' import Image from 'next/image' -import { Color, FontVariant } from '@/app/theme' +import { Color, FontVariant, ScreenSize } from '@/app/theme' import styled from '@emotion/styled' import { MEMBERS } from '@/data/members' import { ResearchTopics } from '@/data/publications' @@ -11,7 +11,7 @@ import { Section, Text } from './Styles' const ResearchTopicsArea = styled.div` display: grid; gap: 48px; - grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); + grid-template-columns: repeat(auto-fit, minmax(min(300px, 45%), 1fr)); ` const ResearchTopicItem = styled.div` @@ -45,7 +45,7 @@ const ResearchTopicsMemberAvatar = styled(Image)` export const ResearchThemesSection = () => { const [membersByResearchTopic, numVisible] = useMemo(() => { - // TODO: Fix the type of membersByResearchTopic + // TODO: Specify the type of membersByResearchTopic const membersByResearchTopic: Record = {} ResearchTopics.forEach(topic => { membersByResearchTopic[topic] = MEMBERS.filter( diff --git a/src/app/homepage/Styles.tsx b/src/app/homepage/Styles.tsx index ac465c7..86098f6 100644 --- a/src/app/homepage/Styles.tsx +++ b/src/app/homepage/Styles.tsx @@ -1,12 +1,18 @@ 'use client' -import { Color, FontVariant } from '@/app/theme' +import { Color, FontVariant, ScreenSize, linearlyScaleSize } from '@/app/theme' import styled from '@emotion/styled' export const Section = styled.section<{ altBackground?: boolean }>` background-color: ${props => (props.altBackground ? '#F6F6F6' : 'white')}; margin: 0 auto; width: 100%; - padding: 48px 96px; + padding: 48px + ${linearlyScaleSize({ + minSizePx: 48, + maxSizePx: 96, + minScreenSizePx: parseInt(ScreenSize.md), + maxScreenSizePx: parseInt(ScreenSize.lg), + })}; ` export const GridContainer = styled.div<{ columnTemplate: string }>` display: grid; diff --git a/src/app/theme.ts b/src/app/theme.ts index 4cce609..7b9ca15 100644 --- a/src/app/theme.ts +++ b/src/app/theme.ts @@ -73,3 +73,28 @@ export const Radius = { sm: '4px', md: '12px', } as const + +export const ScreenSize = { + sm: '576px', + md: '784px', // Navbar looks weird below 784px + lg: '992px', + xl: '1200px', +} as const + +interface LinearlyScaleSizeParams { + minScreenSizePx: number + minSizePx: number + maxScreenSizePx: number + maxSizePx: number +} + +/* Refer to https://www.smashingmagazine.com/2022/01/modern-fluid-typography-css-clamp/ for the math*/ +/* TODO: Specify the return value type of this function*/ +export const linearlyScaleSize = ({ + minScreenSizePx, + minSizePx, + maxScreenSizePx, + maxSizePx, +}: LinearlyScaleSizeParams) => { + return `clamp(${minSizePx}px, calc(${minSizePx}px + ${minSizePx} / ${maxScreenSizePx - minScreenSizePx} * (100vw - ${minScreenSizePx}px)), ${maxSizePx}px)` +} diff --git a/src/components/Filter.tsx b/src/components/Filter.tsx index 4e25925..ca45f78 100644 --- a/src/components/Filter.tsx +++ b/src/components/Filter.tsx @@ -20,9 +20,7 @@ const SelectName = styled.span<{ filtered: boolean }>` const SelectBody = styled.div` position: relative; - min-width: 230px; - width: 100%; - width: fit-content; + min-width: 25vw; ${FontVariant.body_md} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index ba346da..8c73ec3 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -12,6 +12,7 @@ const FooterContainer = styled.footer` align-items: flex-start; padding: 36px 48px; background-color: ${Color.gray600}; + margin-top: auto; ` const FooterTextContainer = styled.div` diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index e8ffddb..3385871 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -3,7 +3,7 @@ import React from 'react' import styled from '@emotion/styled' -import { FontVariant, Color } from '@/app/theme' +import { FontVariant, Color, ScreenSize, linearlyScaleSize } from '@/app/theme' import { usePathname } from 'next/navigation' import Image from 'next/image' import Link from 'next/link' @@ -13,24 +13,32 @@ interface NavItemProps { href: string selected: boolean } - export const NAV_BAR_HEIGHT = 56 export const Nav = styled.nav` position: sticky; top: 0px; - display: flex; - border-bottom: 1px solid ${Color.gray300}; box-sizing: border-box; height: ${NAV_BAR_HEIGHT}px; - padding: 12px 96px 16px 96px; - background-color: ${Color.white}; justify-content: space-between; - align-items: end; - z-index: 1; + align-items: center; + z-index: 2; + + // Prevent the Kixlab logo from suddenly jumping to the left when shrinking the window + // TODO: Replace the magic constants with a more systematic approach + padding: 12px 24px 16px + ${linearlyScaleSize({ + minSizePx: 48, + maxSizePx: 96, + minScreenSizePx: parseInt(ScreenSize.md), + maxScreenSizePx: parseInt(ScreenSize.lg), + })}; + @media (max-width: ${ScreenSize.sm}) { + padding-right: 48px; // Make the padding on the sides equivalent when the hamburger button appears + } ` export const Logo = styled.a` @@ -40,14 +48,39 @@ export const Logo = styled.a` display: flex; align-items: end; gap: 8px; - min-width: 272px; +` + +const NavRow = styled.div` + display: block; + @media (max-width: ${ScreenSize.sm}) { + display: none; + } ` export const NavUl = styled.ul` list-style-type: none; display: flex; - gap: 36px; + gap: 2vw; margin: 0px; + padding: 0px; + @media (max-width: ${ScreenSize.sm}) { + flex-direction: column; + align-items: center; + gap: 0px; + + & > li { + height: 50px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + + &:last-child { + border-bottom: 1px solid ${Color.gray300}; + } + } + } ` const Anchor = styled(Link)<{ selected: boolean }>` @@ -66,7 +99,51 @@ export const NavItem: React.FC = ({ children, href, selected }) => ) +const DropDownMenu = styled.div` + position: fixed; + top: ${NAV_BAR_HEIGHT}; + background-color: ${Color.white}; + width: 100vw; + z-index: 1; + /* Animation */ + transition: transform 0.2s; + &.open { + transform: translateY(0px); + } + &.closed { + transform: translateY(-100%); + } + /* Media queries */ + @media (min-width: ${ScreenSize.sm}) { + display: none; + } + @media (max-width: ${ScreenSize.sm}) { + // TODO: Decide whether to keep centered menu items vs left-aligned menu items + // padding: 12px 48px; + } +` + +const HamburgerButton = styled.button` + display: none; + width: 30px; + background-color: white; + border: none; + cursor: pointer; + @media (max-width: ${ScreenSize.sm}) { + display: block; + } +` + +const ResponsiveSpan = styled.span` + @media (max-width: ${ScreenSize.md}) { + display: none; + } +` + export default function NavBar() { + const [isOpen, setIsOpen] = React.useState(false) + const dropdownRef = React.useRef(null) + const hamburgerRef = React.useRef(null) const NavList = [ { navItem: 'Home', path: '/' }, { navItem: 'People', path: '/people' }, @@ -74,25 +151,61 @@ export default function NavBar() { { navItem: 'Courses', path: '/courses' }, { navItem: 'News', path: '/news' }, ] - const pathname = usePathname() + // Close the dropdown menu whenever the user clicks outside of the dropdown menu area + React.useEffect(() => { + const handleClickOutsideDropDownMenu = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) && + hamburgerRef.current && + !hamburgerRef.current.contains(event.target as Node) + ) { + setIsOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutsideDropDownMenu) + return () => document.removeEventListener('mousedown', handleClickOutsideDropDownMenu) + }, [dropdownRef]) + return ( - + <> + + { + + + {NavList.map((item, i) => ( +
  • + + {item.navItem} + +
  • + ))} +
    +
    + } + ) } diff --git a/src/components/Section.tsx b/src/components/Section.tsx index 769e64b..93fab77 100644 --- a/src/components/Section.tsx +++ b/src/components/Section.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled' -import { FontVariant, Color } from '@/app/theme' +import { FontVariant, Color, ScreenSize, linearlyScaleSize } from '@/app/theme' export const Sections = styled.div` display: flex; @@ -68,15 +68,28 @@ const Title = styled.h2` margin-bottom: 8px; } ` + const Subtitle = styled.h3` ${FontVariant.title_sm} ` +const TitleContainer = styled.div` + display: grid; + gap: 8px; + margin-bottom: clamp(24px, calc(24px + 0.23 * (100vw - ${ScreenSize.md})), 48px); + margin-bottom: ${linearlyScaleSize({ + minSizePx: 24, + maxSizePx: 48, + minScreenSizePx: parseInt(ScreenSize.md), + maxScreenSizePx: parseInt(ScreenSize.lg), + })}; +` + export const SectionHeader: React.FC<{ title: string; subtitle: string }> = ({ title, subtitle }) => { return ( -
    + {title} {subtitle} -
    + ) } diff --git a/src/data/videos.ts b/src/data/videos.ts new file mode 100644 index 0000000..2fc69dc --- /dev/null +++ b/src/data/videos.ts @@ -0,0 +1,36 @@ +interface Props { + url: string + title: string + date: Date +} + +interface Video extends Props {} +class Video { + constructor(attrs: Props) { + Object.assign(this, attrs) + } + + get formattedDate() { + return this.date.toLocaleDateString(navigator.language, { year: 'numeric', month: '2-digit', day: '2-digit' }) + } +} + +const VIDEOS = [ + new Video({ + url: 'https://www.youtube.com/embed/j0v1Cr74kN8?si=tp_blkpKqN4FP9te', + title: 'What is Interaction-centric AI?', + date: new Date('2022-10-28'), + }), + new Video({ + url: 'https://www.youtube.com/embed/pkhTuiYvvw4?si=hUee7IqJ-m95L1k2', + title: 'KIXLAB Introduction', + date: new Date('2021-02-12'), + }), + new Video({ + url: 'https://www.youtube.com/embed/GgvkmXXPFPI?si=YWMLcLMhac5kRYzJ', + title: 'Open KAIST', + date: new Date('2022-01-10'), + }), +] as const + +export default VIDEOS