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 (
+
+
+
+ );
+}
+
+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;