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.
-
+
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 (
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