AmaneSerenetia 1104b76e46 first commit
2024-01-22 20:52:38 +09:00

415 lines
16 KiB
TypeScript

import { useRouter } from "next/router";
import { FC, ReactNode, useEffect, useState } from "react";
import { Box } from "@chakra-ui/react";
import { useAtom } from "jotai";
import { AnimatePresence, motion } from "framer-motion";
import { AvailableLanguages, Language, LanguagePack } from "@states/global";
import { localGet, localSet, Nullable, OverridableStyle, useLocale } from "@utils/common";
import { Forceful, SlowDown } from "@utils/anims";
import { MotionBox } from "./motion";
import { LogoSmall_CN } from "@components/logo/CN/CN-small";
import { LogoSmall_EN } from "@components/logo/EN/EN-small";
import { LogoSmall_JP } from "@components/logo/JP/JP-small";
import { LogoSmall_KR } from "@components/logo/KR/KR-small";
import { Checkbox } from "@components/checkbox";
import { InImageFullScreenMode } from "@components/images";
function resolveConfig(lang: Nullable<string>)
{
switch (lang)
{
case "cn":
return {
marginLeft: "4%",
height: "80%"
};
case "en":
return {
marginLeft: "3%",
height: "85%"
};
case "jp":
return {
marginLeft: "2.5%",
height: "85%"
};
case "kr":
return {
marginLeft: "2.5%",
height: "80%"
};
default:
return {
marginLeft: "auto",
height: "85%"
};
}
}
const transition = {
duration: 0.5,
ease: Forceful
};
export const Header: FC = () => {
const router = useRouter();
const [currentLanguage, setCurrentLanguage] = useAtom(Language);
const [inImageFullScreenMode] = useAtom(InImageFullScreenMode);
const [settingsUIVisible, setSettingsUIVisible] = useState(false);
const [logoStyle, setLogoStyle] = useState(currentLanguage);
let HoverTimeout: Nullable<ReturnType<typeof setTimeout>> = null;
let ToggleTimeout = false;
const handleToggle = (newState: boolean, hover = false) => {
if (ToggleTimeout && !hover)
return;
if (!hover)
{
setSettingsUIVisible(newState);
ToggleTimeout = true;
setTimeout(() => {
ToggleTimeout = false;
}, 500);
}
else {
HoverTimeout = setTimeout(() => {
setSettingsUIVisible(newState);
HoverTimeout = null;
}, 500);
}
};
return (
<AnimatePresence>
<motion.div
className={"abs l0 z5"}
initial={{ top: -90 }}
animate={{ top: inImageFullScreenMode ? -90 : 0 }}
transition={{ duration: 0.5, ease: Forceful }}
>
<Box
h={"88px"}
w={"100vw"}
bg={"#000"}
borderBottom={"1px solid #fff"}
className={"rel flex a-flex-center j-flex-space-between"}
>
<Box {...resolveConfig(logoStyle)}>
<AnimatePresence
exitBeforeEnter
onExitComplete={() => setLogoStyle(currentLanguage)}
>
{currentLanguage === 'cn' && <LogoSmall_CN key={"logo-endfield-cn-smol"}/>}
{currentLanguage === 'en' && <LogoSmall_EN key={"logo-endfield-en-smol"}/>}
{currentLanguage === 'jp' && <LogoSmall_JP key={"logo-endfield-jp-smol"}/>}
{currentLanguage === 'kr' && <LogoSmall_KR key={"logo-endfield-kr-smol"}/>}
</AnimatePresence>
</Box>
<div/>
<Box
className={"flex a-flex-center"}
fontFamily={"Jetbrains Mono"}
onMouseLeave={() => handleToggle(false, true)}
onMouseEnter={() => {
if (HoverTimeout)
clearTimeout(HoverTimeout);
HoverTimeout = null;
}}
>
<AnimatePresence>
{settingsUIVisible &&
<SettingsUI
onLangChange={async (newLang) => {
if (newLang === currentLanguage) return;
router.query.lang = newLang;
await router.push({
pathname: router.pathname,
query: {
...router.query,
lang: newLang
}
}, undefined, { shallow: true });
setCurrentLanguage(newLang);
}}
/>
}
</AnimatePresence>
<SettingsToggle
active={settingsUIVisible}
onClick={() => handleToggle(!settingsUIVisible)}
/>
</Box>
</Box>
</motion.div>
</AnimatePresence>
);
};
interface ISettingsToggle {
active: boolean;
onClick: () => void;
}
const SettingsToggle: FC<ISettingsToggle> = ({ active, onClick }) => {
const locale = useLocale(useAtom(Language)[0], useAtom(LanguagePack)[0]);
const [isHover, setIsHover] = useState(false);
const [isSmall, setIsSmall] = useState(false);
useEffect(() => {
window.addEventListener('resize', () => {
if (isSmall === window.innerWidth < 720) return;
setIsSmall(window.innerWidth < 720);
});
}, [window.innerWidth]);
return (
<button
onClick={onClick}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
className={"rel exclusion"}
>
<MotionBox
initial={{
padding: "0.5rem 1.5rem 0.5rem 0.5rem",
}}
animate={{
paddingRight: isSmall ? "0.2rem" : "1.5rem",
}}
gap={"2px"}
color={"#fff"}
className={"flex a-flex-center j-flex-center"}
transition={transition}
layout={"position"}
>
<SettingsIcon/>
<AnimatePresence>
<motion.p
initial={{ opacity: 0, width: 0 }}
animate={{ opacity: 1, width: isSmall ? 0 : "100%" }}
exit={{ opacity: 0, width: 0 }}
transition={transition}
style={{whiteSpace: "nowrap", overflow: "hidden"}}
layout={"position"}
>
{locale("settings.label")}
</motion.p>
</AnimatePresence>
</MotionBox>
<motion.div
className={"abs fh t0 r0"}
animate={{
background: "#fff",
width: "0.5em",
x: (active || isHover) ? 0 : "0.5em",
}}
transition={transition}
/>
</button>
);
};
const settingsUIStates = {
hidden: {
y: "-100%",
transition: {
delay: 0.12,
...transition,
}
},
visible: {
y: 0,
transition: {
...transition,
staggerChildren: 0.3,
}
},
visibleSmall: {
y: 88,
transition,
},
childHidden: {
opacity: 0,
y: -10,
transition,
},
childVisible: {
opacity: 1,
y: 0,
transition: {
delay: 0.3,
duration: 0.5,
ease: SlowDown,
}
}
};
const SettingsItem: FC<{ children?: ReactNode } & OverridableStyle> = (
{
children = null,
overrideStyles
}
) => {
return (
<motion.div
className={"rel fw flex a-flex-center j-flex-space-between"}
style={{
padding: "4px",
...overrideStyles
}}
variants={settingsUIStates}
initial="childHidden"
animate="childVisible"
exit="childHidden"
layout={"position"}
>
{children}
</motion.div>
);
};
interface ISettings {
onLangChange: (newLang: string) => void;
}
const SettingsUI: FC<ISettings> = ({ onLangChange }) => {
const [lang] = useAtom(Language);
const locale = useLocale(lang, useAtom(LanguagePack)[0]);
const router = useRouter();
const [fullIntro, setFullIntro] = useState(false);
const [currentLanguage, setCurrentLanguage] = useState(lang);
const availableLangs = AvailableLanguages.map(l => l.toUpperCase());
const btnVariants = {
langBtn: {
opacity: 1,
color: "#fff",
backgroundColor: "#000",
},
langBtnHover: {
color: "#000",
backgroundColor: "#fff",
},
langBtnInactive: {
color: "#fff",
backgroundColor: "#626262",
}
};
const updateLanguage = (newLang: string) => {
setCurrentLanguage(newLang);
onLangChange(newLang);
};
useEffect(() => {
setFullIntro(localGet("fullIntro") === "true");
}, [fullIntro, currentLanguage]);
const fullIntroSTChange = (newValue: boolean) => {
setFullIntro(newValue);
localSet("fullIntro", (newValue).toString());
void router.push({
pathname: "/",
query: Object.fromEntries(Object.entries({
lang: currentLanguage,
fullIntro: newValue ? newValue : undefined
}).filter(([, value]) => !!value))
}, undefined, { shallow: true });
};
return (
<motion.div
variants={settingsUIStates}
initial={"hidden"}
animate={["visible", window.innerWidth < 720 ? "visibleSmall" : ""]}
exit={"hidden"}
className={"abs t0 r0 flex flex-col"}
style={{
background: "#fff",
// mixBlendMode: window.innerWidth < 720 ? "exclusion" : "normal"
}}
layout
>
<Box className={"fw"} h={1} padding={`${window.innerWidth < 720 ? 0 : 82}px 7px 5px 7px`}/>
<SettingsItem>
<Box as={"p"} fontFamily={"Jetbrains Mono"} p={4}>{locale("language")}</Box>
<Box fontFamily={"Jetbrains Mono"} p={4} className={"flex"}>
{availableLangs.map((_lang, i) => {
const sameLanguage = _lang.toLowerCase() === currentLanguage;
return (
<motion.button
key={i}
onClick={() => updateLanguage(_lang.toLowerCase())}
variants={btnVariants}
initial={sameLanguage ? "langBtnInactive" : "langBtn"}
whileHover={sameLanguage ? "langBtnInactive" : "langBtnHover"}
animate={sameLanguage ? "langBtnInactive" : "langBtn"}
className={sameLanguage ? "no-pointer" : ""}
style={{padding: "0.1em"}}
>
{_lang}
</motion.button>
);
})}
</Box>
</SettingsItem>
<SettingsItem overrideStyles={{ paddingTop: 0, paddingBottom: 8, paddingLeft: 0 }}>
<Box paddingLeft={5}>{locale("settings.full-intro.title")}</Box>
<Box paddingInline={4}>
<Checkbox
checked={fullIntro}
h={"25px"}
w={"26px"}
onChange={fullIntroSTChange}
bg={"#fff"}
/>
</Box>
<Box
as={"i"}
className={"abs fw txt-algn-center"}
fontSize={"0.75rem"} top={"73%"}
>
{locale("settings.full-intro.desc")}
</Box>
</SettingsItem>
<Box className={"fw"} h={1} padding={"7px"}/>
</motion.div>
);
};
const SettingsIcon = () => {
return (
<motion.svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 300.112 300.112"
width="1.5rem"
height="1.5rem"
>
<path fill="#fff" d="M150.057,105.1c-24.789,0-44.955,20.167-44.955,44.955s20.166,44.955,44.955,44.955
c24.789,0,44.955-20.167,44.955-44.955S174.845,105.1,150.057,105.1z M150.057,178.36c-15.607,0-28.305-12.697-28.305-28.305
s12.697-28.305,28.305-28.305c15.608,0,28.305,12.697,28.305,28.305S165.663,178.36,150.057,178.36z"/>
<path fill="#fff" d="M297.365,183.342l-25.458-22.983v-20.608l25.457-22.981c2.614-2.361,3.461-6.112,2.112-9.366l-13.605-32.846
c-1.348-3.253-4.588-5.305-8.115-5.128l-34.252,1.749l-14.571-14.571l1.749-34.251c0.18-3.518-1.874-6.769-5.128-8.116
L192.707,0.635c-3.253-1.35-7.005-0.501-9.365,2.111l-22.984,25.458h-20.606L116.77,2.746c-2.361-2.613-6.112-3.458-9.365-2.111
L74.559,14.24c-3.255,1.348-5.308,4.599-5.128,8.116l1.75,34.251L56.609,71.178l-34.252-1.749
c-3.506-0.188-6.768,1.874-8.115,5.128L0.635,107.403c-1.348,3.255-0.502,7.005,2.112,9.366l25.457,22.981v20.608L2.749,183.341
c-2.614,2.361-3.461,6.112-2.112,9.366l13.605,32.846c1.348,3.255,4.603,5.321,8.115,5.128l34.252-1.749l14.572,14.571
l-1.75,34.251c-0.18,3.518,1.874,6.769,5.128,8.116l32.846,13.606c3.255,1.352,7.005,0.502,9.365-2.111l22.984-25.458h20.606
l22.984,25.458c1.613,1.785,3.873,2.746,6.182,2.746c1.071,0,2.152-0.208,3.183-0.634l32.846-13.606
c3.255-1.348,5.308-4.599,5.128-8.116l-1.749-34.251l14.571-14.571l34.252,1.749c3.506,0.178,6.768-1.874,8.115-5.128
l13.605-32.846C300.825,189.453,299.979,185.703,297.365,183.342z M272.737,213.754l-32.079-1.639
c-2.351-0.127-4.646,0.764-6.311,2.428l-19.804,19.804c-1.666,1.666-2.547,3.958-2.428,6.311l1.638,32.079l-21.99,9.109
l-21.525-23.843c-1.578-1.747-3.824-2.746-6.179-2.746h-28.006c-2.355,0-4.601,0.998-6.179,2.746l-21.525,23.843l-21.99-9.109
l1.639-32.079c0.12-2.353-0.763-4.646-2.429-6.311l-19.803-19.804c-1.665-1.665-3.955-2.55-6.311-2.428l-32.079,1.639
l-9.109-21.99l23.842-21.525c1.748-1.58,2.746-3.824,2.746-6.179v-28.008c0-2.355-0.998-4.601-2.746-6.179l-23.842-21.525
l9.109-21.99l32.079,1.639c2.354,0.124,4.646-0.763,6.311-2.428l19.803-19.803c1.666-1.666,2.549-3.958,2.429-6.313
l-1.639-32.079l21.99-9.109l21.525,23.842c1.578,1.747,3.824,2.746,6.179,2.746h28.006c2.355,0,4.601-0.998,6.179-2.746
l21.525-23.842l21.99,9.109l-1.638,32.079c-0.12,2.353,0.761,4.645,2.428,6.313l19.804,19.803
c1.666,1.665,3.959,2.564,6.311,2.428l32.079-1.639l9.109,21.99l-23.843,21.525c-1.748,1.58-2.746,3.824-2.746,6.179v28.008
c0,2.355,0.998,4.601,2.746,6.179l23.843,21.525L272.737,213.754z"/>
<path fill="#fff" d="M150.057,71.357c-43.394,0-78.698,35.305-78.698,78.698c0,43.394,35.304,78.698,78.698,78.698
c43.394,0,78.698-35.305,78.698-78.698C228.754,106.661,193.45,71.357,150.057,71.357z M150.057,212.103
c-34.214,0-62.048-27.834-62.048-62.048c0-34.214,27.834-62.048,62.048-62.048c34.214,0,62.048,27.834,62.048,62.048
C212.105,184.269,184.269,212.103,150.057,212.103z"/>
</motion.svg>
);
};