diff --git a/src/app/about/Goals/index.tsx b/src/app/about/approach/Goals/index.tsx similarity index 100% rename from src/app/about/Goals/index.tsx rename to src/app/about/approach/Goals/index.tsx diff --git a/src/app/about/Goals/styles.module.css b/src/app/about/approach/Goals/styles.module.css similarity index 100% rename from src/app/about/Goals/styles.module.css rename to src/app/about/approach/Goals/styles.module.css diff --git a/src/app/about/Values/index.tsx b/src/app/about/approach/Values/index.tsx similarity index 100% rename from src/app/about/Values/index.tsx rename to src/app/about/approach/Values/index.tsx diff --git a/src/app/about/Values/styles.module.css b/src/app/about/approach/Values/styles.module.css similarity index 100% rename from src/app/about/Values/styles.module.css rename to src/app/about/approach/Values/styles.module.css diff --git a/src/app/about/VisionMission/index.tsx b/src/app/about/approach/VisionMission/index.tsx similarity index 100% rename from src/app/about/VisionMission/index.tsx rename to src/app/about/approach/VisionMission/index.tsx diff --git a/src/app/about/VisionMission/styles.module.css b/src/app/about/approach/VisionMission/styles.module.css similarity index 100% rename from src/app/about/VisionMission/styles.module.css rename to src/app/about/approach/VisionMission/styles.module.css diff --git a/src/app/about/page.module.css b/src/app/about/approach/page.module.css similarity index 100% rename from src/app/about/page.module.css rename to src/app/about/approach/page.module.css diff --git a/src/app/about/approach/page.tsx b/src/app/about/approach/page.tsx new file mode 100644 index 0000000..86cbc77 --- /dev/null +++ b/src/app/about/approach/page.tsx @@ -0,0 +1,33 @@ +import Banner from '#components/Banner'; +import Divider from '#components/Divider'; +import Page from '#components/Page'; +import AboutUsImage from '#public/aboutUsImage.jpg'; + +import Goals from './Goals'; +import Values from './Values'; +import VisionMission from './VisionMission'; + +import styles from './page.module.css'; + +export default function About() { + return ( + + + Empowering Change Through +
+ Gender-Equal Citizenship + + )} + /> + + + + +
+ ); +} diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 86cbc77..2297a1f 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,33 +1,5 @@ -import Banner from '#components/Banner'; -import Divider from '#components/Divider'; -import Page from '#components/Page'; -import AboutUsImage from '#public/aboutUsImage.jpg'; +import { redirect } from 'next/navigation'; -import Goals from './Goals'; -import Values from './Values'; -import VisionMission from './VisionMission'; - -import styles from './page.module.css'; - -export default function About() { - return ( - - - Empowering Change Through -
- Gender-Equal Citizenship - - )} - /> - - - - -
- ); +export default function AboutRedirect() { + redirect('/about/approach'); } diff --git a/src/app/globals.css b/src/app/globals.css index f114c20..b409083 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -41,6 +41,10 @@ --border-radius-small: 12px; --border-radius-extra-small: 8px; + --border-radius-popup: 3px; + --blur-radius-popup: 1px; + --color-shadow: rgba(0,0,0,.5); + /* Font size */ --font-size-super-small: calc(var(--font-size-base) / (var(--multiplier) * var(--multiplier) * var(--multiplier))); --font-size-extra-small: calc(var(--font-size-base) / (var(--multiplier) * var(--multiplier))); diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index 034f9b5..aa167a1 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -13,6 +13,8 @@ import Button from '#components/Button'; import Link from '#components/Link'; import logo from '#public/logo.png'; +import PopupButton from '../PopupButton'; + import styles from './styles.module.css'; interface Props { @@ -43,13 +45,32 @@ export default function Navbar(props: Props) { />
- - About - + + Our Approach + + + Our Journey + + + Our Members + + ): FloatingPlacementProps { + const placement = { + ...defaultPlacement, + }; + + let horizontalPosition: 'left' | 'right' | undefined; + let verticalPosition: 'bottom' | 'top' | undefined; + let contentWidth = 'auto'; + let maxHeight = 'auto'; + + if (parentRef?.current) { + const parentBCR = parentRef.current.getBoundingClientRect(); + const { + x, y, width, height, + } = parentBCR; + + const cX = window.innerWidth / 2; + const cY = window.innerHeight / 2; + + horizontalPosition = (cX - parentBCR.x) > 0 ? 'right' : 'left'; + verticalPosition = (cY - parentBCR.y) > 0 ? 'bottom' : 'top'; + + if (horizontalPosition === 'left') { + placement.right = `${window.innerWidth - x - width}px`; + } else if (horizontalPosition === 'right') { + placement.left = `${x}px`; + } + + if (verticalPosition === 'top') { + placement.bottom = `${window.innerHeight - y + 10}px`; + } else if (verticalPosition === 'bottom') { + placement.top = `${y + height + 10}px`; + } + + contentWidth = `${width}px`; + maxHeight = `calc(50vh - ${height / 2}px)`; + } + + return { + placement, + width: contentWidth, + horizontalPosition, + verticalPosition, + maxHeight, + }; +} + +function useAttachedFloatingPlacement(parentRef: React.RefObject) { + const [placement, setPlacement] = useState({ + placement: defaultPlacement, + width: 'auto', + maxHeight: 'auto', + horizontalPosition: 'left', + verticalPosition: 'top', + }); + + useLayoutEffect(() => { + setPlacement(getFloatPlacement(parentRef)); + }, [setPlacement, parentRef]); + + // FIXME: throttle + const handleScroll = useCallback(() => { + setPlacement(getFloatPlacement(parentRef)); + }, [setPlacement, parentRef]); + + // FIXME: throttle + const handleResize = useCallback(() => { + setPlacement(getFloatPlacement(parentRef)); + }, [setPlacement, parentRef]); + + useEffect(() => { + document.addEventListener('scroll', handleScroll, true); + window.addEventListener('resize', handleResize, true); + + return () => { + document.removeEventListener('scroll', handleScroll, true); + window.removeEventListener('resize', handleResize, true); + }; + }, [handleScroll, handleResize]); + + return placement; +} + +export interface PopupProps { + className?: string; + contentClassName?: string; + parentRef: React.RefObject; + elementRef?: React.RefObject; + children: React.ReactNode; + freeWidth?: boolean; +} + +function Popup(props: PopupProps) { + const { + parentRef, + children, + className, + contentClassName, + freeWidth, + elementRef, + } = props; + + const { + placement, + width, + horizontalPosition, + verticalPosition, + maxHeight, + } = useAttachedFloatingPlacement(parentRef); + + return ( + +
+
+
+ {children} +
+
+ + ); +} + +export default Popup; diff --git a/src/components/Popup/styles.module.css b/src/components/Popup/styles.module.css new file mode 100644 index 0000000..3b4c574 --- /dev/null +++ b/src/components/Popup/styles.module.css @@ -0,0 +1,54 @@ +.popup { + position: fixed; + z-index: 999999; + border-radius: var(--border-radius-popup); + background-color: var(--color-background); + filter: drop-shadow(0 0 var(--blur-radius-popup) var(--color-shadow)); + max-width: calc(100vw - 2 * var(--spacing-medium)); + + --height-tip: 10px; + --width-tip: 20px; + + .tip { + position: absolute; + top: unset; + right: unset; + bottom: unset; + left: unset; + background-color: #fff; + width: var(--width-tip); + height: var(--height-tip); + } + + &.top { + .tip { + bottom: calc(-1 * var(--height-tip)); + clip-path: polygon(100% 0%, 0 0, 50% 100%); + } + } + + &.bottom { + .tip { + top: calc(-1 * var(--height-tip)); + clip-path: polygon(50% 0%, 0% 100%, 100% 100%); + } + } + + &.left { + .tip { + right: var(--width-tip); + } + } + + &.right { + .tip { + left: var(--width-tip); + } + } + + .content { + width: 100%; + height: 100%; + overflow: auto; + } +} diff --git a/src/components/PopupButton/index.tsx b/src/components/PopupButton/index.tsx new file mode 100644 index 0000000..c7cf92c --- /dev/null +++ b/src/components/PopupButton/index.tsx @@ -0,0 +1,109 @@ +import React, { + useCallback, + useEffect, +} from 'react'; +import { + IoIosArrowDown, + IoIosArrowUp, +} from 'react-icons/io'; +import { _cs } from '@togglecorp/fujs'; + +import useBlurEffect from '../../hooks/useBlurEffect'; +import Button, { type Props as ButtonProps } from '../Button'; +import Popup from '../Popup'; + +import styles from './styles.module.css'; + +export interface PopupButtonProps extends Omit { + popupClassName?: string; + popupContentClassName?: string; + label: React.ReactNode; + componentRef?: React.RefObject<{ + setPopupVisibility: React.Dispatch>; + } | null>; + persistent: boolean; + arrowHidden?: boolean; + defaultShown?: boolean; + elementRef?: React.RefObject; + actions?: React.ReactNode; +} +function PopupButton(props: PopupButtonProps) { + const { + popupClassName, + popupContentClassName, + children, + label, + name, + actions, + componentRef, + arrowHidden, + persistent, + defaultShown, + elementRef, + ...otherProps + } = props; + + const internalButtonRef = React.useRef(null); + const popupRef = React.useRef(null); + + const buttonRef = elementRef ?? internalButtonRef; + + const [popupShown, setPopupShown] = React.useState(defaultShown ?? false); + + useEffect( + () => { + if (componentRef) { + componentRef.current = { + setPopupVisibility: setPopupShown, + }; + } + }, + [componentRef], + ); + + useBlurEffect( + popupShown && !persistent, + setPopupShown, + popupRef, + buttonRef, + ); + + const handleShowPopup = useCallback( + () => { + setPopupShown((prevState) => !prevState); + }, + [], + ); + + return ( + <> + + {popupShown && ( + + {children} + + )} + + ); +} + +export default PopupButton; diff --git a/src/components/PopupButton/styles.module.css b/src/components/PopupButton/styles.module.css new file mode 100644 index 0000000..9eea15c --- /dev/null +++ b/src/components/PopupButton/styles.module.css @@ -0,0 +1,16 @@ +.popupButton { + display: flex; + gap: var(--spacing-tiny); + align-items: center; + color: var(--color-text); +} + +.popup { + padding: calc(var(--spacing-medium) - var(--spacing-small)); + + .popupContent { + display: flex; + flex-direction: column; + max-width: max(50vw, 300px); + } +} diff --git a/src/components/Portal.tsx b/src/components/Portal.tsx new file mode 100644 index 0000000..610f7e4 --- /dev/null +++ b/src/components/Portal.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +export interface PortalProps { + children: React.ReactNode; +} + +function Portal(props: PortalProps) { + const { children } = props; + return ( + <> + {ReactDOM.createPortal( + children, + document.body, + )} + + ); +} + +export default Portal; diff --git a/src/hooks/useBlurEffect.tsx b/src/hooks/useBlurEffect.tsx new file mode 100644 index 0000000..c10b75b --- /dev/null +++ b/src/hooks/useBlurEffect.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +function useBlurEffect( + shouldWatch: boolean, + callback: (isClickedWithin: boolean, e: MouseEvent) => void, + elementRef: React.RefObject, + parentRef: React.RefObject, +) { + React.useEffect( + () => { + if (!shouldWatch) { + return undefined; + } + + const handleDocumentClick = (e: MouseEvent) => { + const { current: element } = elementRef; + const { current: parent } = parentRef; + + const isElementOrContainedInElement = element + ? element === e.target || element.contains(e.target as HTMLElement) + : false; + const isParentOrContainedInParent = parent + ? parent === e.target || parent.contains(e.target as HTMLElement) + : false; + + const clickedInside = isElementOrContainedInElement || isParentOrContainedInParent; + + callback(clickedInside, e); + }; + + document.addEventListener('click', handleDocumentClick, true); + + return () => { + document.removeEventListener('click', handleDocumentClick, true); + }; + }, + [shouldWatch, callback, elementRef, parentRef], + ); +} +export default useBlurEffect;