-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add language switch & enable 5 languages
Closes #134
- Loading branch information
Showing
9 changed files
with
326 additions
and
8 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { useEffect, useState } from 'react' | ||
|
||
const useStateWithLocalStorage = <T extends string>(localStorageKey: string, defaultValue: T) => { | ||
const [value, setValue] = useState(localStorage.getItem(localStorageKey) || defaultValue) | ||
|
||
useEffect(() => { | ||
localStorage.setItem(localStorageKey, value) | ||
}, [localStorageKey, value]) | ||
|
||
return [value as T, setValue] as const | ||
} | ||
|
||
export default useStateWithLocalStorage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import i18next from "i18next"; | ||
import { useEffect } from "react"; | ||
import styled from "styled-components"; | ||
|
||
import { Language, languageOptions } from "./languages"; | ||
import useStateWithLocalStorage from "../hooks/useStateWithLocalStorage"; | ||
import Menu from "./Menu"; | ||
|
||
interface LanguageSwitchProps { | ||
className?: string; | ||
} | ||
|
||
const LanguageSwitch: React.FC<LanguageSwitchProps> = ({ className }) => { | ||
const [langValue, setLangValue] = useStateWithLocalStorage<Language>('language', 'en') | ||
|
||
useEffect(() => { | ||
i18next.changeLanguage(langValue) | ||
}, [langValue]) | ||
|
||
const items = languageOptions.map((lang) => ({ | ||
text: lang.label, | ||
onClick: () => setLangValue(lang.value) | ||
})) | ||
|
||
return ( | ||
<Menu | ||
aria-label="Language" | ||
label={languageOptions.find((o) => o.value === langValue)?.label || ''} | ||
items={items} | ||
direction="up" | ||
className={className} | ||
/> | ||
); | ||
}; | ||
|
||
export default styled(LanguageSwitch)` | ||
border-radius: 8px; | ||
background-color: #1B1B1F; | ||
border: 1px solid rgba(255, 255, 255, 0.08); | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import { AnimatePresence, motion } from 'framer-motion' | ||
import { useState } from 'react' | ||
import { RiMore2Line } from 'react-icons/ri' | ||
import styled from 'styled-components' | ||
|
||
interface MenuItem { | ||
text: string | ||
icon?: React.ReactNode | ||
onClick: () => void | ||
} | ||
|
||
type Direction = 'up' | 'down' | ||
|
||
const menuHeight = '45px' | ||
|
||
const Menu = ({ | ||
label, | ||
icon, | ||
items, | ||
direction, | ||
className | ||
}: { | ||
label: string | ||
icon?: React.ReactNode | ||
items: MenuItem[] | ||
direction: Direction | ||
className?: string | ||
}) => { | ||
const [visible, setVisible] = useState(false) | ||
|
||
const animationOrigin = direction === 'up' ? '-95%' : `calc(${menuHeight} - 10px)` | ||
const animationDestination = direction === 'up' ? '-100%' : menuHeight | ||
|
||
const handleBlur = () => { | ||
setVisible(false) | ||
} | ||
|
||
return ( | ||
<MenuContainer | ||
onClick={() => setVisible(!visible)} | ||
className={className} | ||
id="menu-container" | ||
onBlur={handleBlur} | ||
tabIndex={0} | ||
> | ||
<MenuCurrentContent> | ||
{icon && <IconContainer>{icon}</IconContainer>} | ||
<Label>{label}</Label> | ||
<RiMore2Line size={15} /> | ||
</MenuCurrentContent> | ||
<AnimatePresence> | ||
{visible && ( | ||
<MenuItemsContainer | ||
initial={{ y: animationOrigin, opacity: 0 }} | ||
animate={{ y: animationDestination, opacity: 1 }} | ||
exit={{ y: animationOrigin, opacity: 0 }} | ||
transition={{ duration: 0.15 }} | ||
> | ||
<MenuItemsList | ||
style={{ marginBottom: direction === 'up' ? '8px' : 0, marginTop: direction === 'down' ? '8px' : 0 }} | ||
> | ||
{items.map((item, i) => ( | ||
<div key={i}> | ||
<MenuItemComponent onClick={item.onClick}> | ||
{item.icon && <ItemIcon>{item.icon}</ItemIcon>} | ||
<ItemText>{item.text}</ItemText> | ||
</MenuItemComponent> | ||
{i !== items.length - 1 && <Divider />} | ||
</div> | ||
))} | ||
</MenuItemsList> | ||
</MenuItemsContainer> | ||
)} | ||
</AnimatePresence> | ||
</MenuContainer> | ||
) | ||
} | ||
|
||
export default Menu | ||
|
||
const MenuContainer = styled.div` | ||
position: relative; | ||
height: ${menuHeight}; | ||
display: flex; | ||
outline: none; | ||
&:hover { | ||
background-color: rgba(255, 255, 255, 0.02); | ||
} | ||
` | ||
|
||
const MenuCurrentContent = styled.div` | ||
flex: 1; | ||
display: flex; | ||
align-items: center; | ||
padding: 0 15px; | ||
cursor: pointer; | ||
gap: 15px; | ||
` | ||
|
||
const Label = styled.span` | ||
color: #e3e3e3; | ||
line-height: initial; | ||
flex: 1; | ||
` | ||
|
||
const IconContainer = styled.div`` | ||
|
||
const MenuItemsContainer = styled(motion.div)` | ||
position: absolute; | ||
width: 100%; | ||
z-index: 10000; | ||
` | ||
|
||
const MenuItemsList = styled.div` | ||
overflow: hidden; | ||
border-radius: 8px; | ||
background-color: #1B1B1F; | ||
border: 1px solid rgba(255, 255, 255, 0.08); | ||
` | ||
|
||
const ItemIcon = styled.div` | ||
width: 23px; | ||
height: 23px; | ||
margin-right: 20px; | ||
opacity: 0.8; | ||
` | ||
|
||
const MenuItemComponent = styled.div` | ||
height: 47px; | ||
display: flex; | ||
align-items: center; | ||
padding: 0 20px; | ||
cursor: pointer; | ||
color: #e3e3e3; | ||
&:hover { | ||
background-color: rgba(255, 255, 255, 0.02); | ||
color: #598BED; | ||
${ItemIcon} { | ||
opacity: 1; | ||
} | ||
} | ||
` | ||
|
||
const ItemText = styled.div` | ||
text-align: left; | ||
` | ||
|
||
const Divider = styled.div` | ||
height: 1px; | ||
background-color: rgba(255, 255, 255, 0.04); | ||
` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import i18next from "i18next"; | ||
import LanguageDetector from "i18next-browser-languagedetector"; | ||
import { initReactI18next } from "react-i18next"; | ||
|
||
import de from "../../locales/de-DE/translation.json"; | ||
import el from "../../locales/el-GR/translation.json"; | ||
import en from "../../locales/en-US/translation.json"; | ||
import id from "../../locales/id-ID/translation.json"; | ||
import vi from "../../locales/vi-VN/translation.json"; | ||
import pt from "../../locales/pt-PT/translation.json"; | ||
import { supportedLanguages } from "./languages"; | ||
|
||
i18next | ||
.use(initReactI18next) | ||
.use(LanguageDetector) | ||
.init({ | ||
resources: { | ||
en: { translation: en }, | ||
id: { translation: id }, | ||
el: { translation: el }, | ||
de: { translation: de }, | ||
vi: { translation: vi }, | ||
pt: { translation: pt }, | ||
}, | ||
supportedLngs: supportedLanguages, | ||
fallbackLng: "en", | ||
detection: { | ||
lookupLocalStorage: "language", | ||
}, | ||
interpolation: { | ||
escapeValue: false, | ||
}, | ||
}); | ||
|
||
export default i18next; |
Oops, something went wrong.