Skip to content

Commit aecae90

Browse files
committed
moblie menu
1 parent a0151bf commit aecae90

File tree

4 files changed

+325
-26
lines changed

4 files changed

+325
-26
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use client"
2+
3+
import { Languages, Search as SearchIcon } from "lucide-react"
4+
5+
import Search from "@/components/Search"
6+
7+
import { MOBILE_LANGUAGE_BUTTON_NAME } from "@/lib/constants"
8+
9+
import FooterButton from "./FooterButton"
10+
import FooterItemText from "./FooterItemText"
11+
import { useMobileMenu } from "./MobileMenuContent"
12+
import ThemeToggleFooterButton from "./ThemeToggleFooterButton"
13+
14+
import { useTranslation } from "@/hooks/useTranslation"
15+
16+
const MenuFooterClient = () => {
17+
const { t } = useTranslation("common")
18+
const { setCurrentView } = useMobileMenu()
19+
20+
const handleLanguageClick = () => {
21+
setCurrentView("language-picker")
22+
}
23+
24+
return (
25+
<div className="grid w-full grid-cols-3 items-center justify-center">
26+
<Search asChild>
27+
<FooterButton icon={SearchIcon}>
28+
<FooterItemText>{t("search")}</FooterItemText>
29+
</FooterButton>
30+
</Search>
31+
32+
<ThemeToggleFooterButton />
33+
34+
<FooterButton
35+
icon={Languages}
36+
name={MOBILE_LANGUAGE_BUTTON_NAME}
37+
onClick={handleLanguageClick}
38+
>
39+
<FooterItemText>{t("languages")}</FooterItemText>
40+
</FooterButton>
41+
</div>
42+
)
43+
}
44+
45+
export default MenuFooterClient
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
"use client"
2+
3+
import { memo } from "react"
4+
import { ChevronLeft } from "lucide-react"
5+
import { useParams } from "next/navigation"
6+
import { useLocale } from "next-intl"
7+
8+
import type { LocaleDisplayInfo } from "@/lib/types"
9+
10+
import { ButtonLink } from "@/components/ui/buttons/Button"
11+
import { Button } from "@/components/ui/buttons/Button"
12+
13+
import { DEFAULT_LOCALE } from "@/lib/constants"
14+
15+
import ClientMenuItem from "../../LanguagePicker/ClientMenuItem"
16+
import NoResultsCallout from "../../LanguagePicker/NoResultsCallout"
17+
import { useLanguagePicker } from "../../LanguagePicker/useLanguagePicker"
18+
import {
19+
Command,
20+
CommandEmpty,
21+
CommandGroup,
22+
CommandInput,
23+
CommandList,
24+
} from "../../ui/command"
25+
26+
import { useMobileMenu } from "./MobileMenuContent"
27+
28+
import { useTranslation } from "@/hooks/useTranslation"
29+
import { usePathname, useRouter } from "@/i18n/routing"
30+
31+
type MobileLanguagePickerProps = {
32+
languages: LocaleDisplayInfo[]
33+
}
34+
35+
const MobileLanguagePicker = memo(
36+
({ languages }: MobileLanguagePickerProps) => {
37+
const { setCurrentView } = useMobileMenu()
38+
const pathname = usePathname()
39+
const { push } = useRouter()
40+
const params = useParams()
41+
const { languages: sortedLanguages, intlLanguagePreference } =
42+
useLanguagePicker(languages)
43+
44+
const handleBackClick = () => {
45+
setCurrentView("menu")
46+
}
47+
48+
const handleMenuItemSelect = (currentValue: string) => {
49+
push(
50+
// @ts-expect-error -- TypeScript will validate that only known `params`
51+
// are used in combination with a given `pathname`. Since the two will
52+
// always match for the current route, we can skip runtime checks.
53+
{ pathname, params },
54+
{
55+
locale: currentValue,
56+
}
57+
)
58+
// Close the sheet by going back to menu view
59+
setCurrentView("menu")
60+
}
61+
62+
const handleNoResultsClose = () => {
63+
// Navigate to translation program or handle as needed
64+
}
65+
66+
const handleTranslationProgramClick = () => {
67+
// Navigate to translation program
68+
}
69+
70+
return (
71+
<div className="flex h-full flex-col">
72+
{/* Back navigation */}
73+
<div className="border-b border-body-light p-4">
74+
<Button
75+
variant="ghost"
76+
size="sm"
77+
onClick={handleBackClick}
78+
className="flex items-center gap-2 p-0 text-body"
79+
>
80+
<ChevronLeft className="h-4 w-4" />
81+
Back
82+
</Button>
83+
</div>
84+
85+
{/* Language picker menu */}
86+
<div className="flex-1 overflow-auto">
87+
<LanguagePickerMenu
88+
languages={sortedLanguages}
89+
onSelect={handleMenuItemSelect}
90+
onClose={handleNoResultsClose}
91+
/>
92+
</div>
93+
94+
{/* Footer */}
95+
<LanguagePickerFooter
96+
intlLanguagePreference={intlLanguagePreference}
97+
onTranslationProgramClick={handleTranslationProgramClick}
98+
/>
99+
</div>
100+
)
101+
}
102+
)
103+
104+
MobileLanguagePicker.displayName = "MobileLanguagePicker"
105+
106+
const LanguagePickerMenu = ({ languages, onClose, onSelect }) => {
107+
const { t } = useTranslation("common")
108+
109+
return (
110+
<Command
111+
className="max-h-full gap-2 p-4"
112+
filter={(value: string, search: string) => {
113+
const item = languages.find((name) => name.localeOption === value)
114+
115+
if (!item) return 0
116+
117+
const { localeOption, sourceName, targetName, englishName } = item
118+
119+
if (
120+
(localeOption + sourceName + targetName + englishName)
121+
.toLowerCase()
122+
.includes(search.toLowerCase())
123+
) {
124+
return 1
125+
}
126+
127+
return 0
128+
}}
129+
>
130+
<div className="text-xs text-body-medium">
131+
{t("page-languages-filter-label")}{" "}
132+
<span className="lowercase">
133+
({languages.length} {t("common:languages")})
134+
</span>
135+
</div>
136+
137+
<CommandInput
138+
placeholder={t("page-languages-filter-placeholder")}
139+
className="h-9"
140+
kbdShortcut="\"
141+
/>
142+
143+
<CommandList className="max-h-full">
144+
<CommandEmpty className="py-0 text-left text-base">
145+
<NoResultsCallout onClose={onClose} />
146+
</CommandEmpty>
147+
<CommandGroup className="p-0">
148+
{languages.map((displayInfo) => (
149+
<ClientMenuItem
150+
key={"item-" + displayInfo.localeOption}
151+
displayInfo={displayInfo}
152+
onSelect={onSelect}
153+
/>
154+
))}
155+
</CommandGroup>
156+
</CommandList>
157+
</Command>
158+
)
159+
}
160+
161+
const LanguagePickerFooter = ({
162+
intlLanguagePreference,
163+
onTranslationProgramClick,
164+
}: {
165+
intlLanguagePreference?: LocaleDisplayInfo
166+
onTranslationProgramClick: () => void
167+
}) => {
168+
const { t } = useTranslation("common")
169+
const locale = useLocale()
170+
171+
return (
172+
<div className="sticky bottom-0 flex border-t-2 border-primary bg-primary-low-contrast p-0 pb-1 pt-1">
173+
<div className="flex w-full max-w-sm items-center justify-between px-4">
174+
<div className="flex min-w-0 flex-col items-start">
175+
{locale === DEFAULT_LOCALE ? (
176+
<p className="overflow-hidden text-ellipsis whitespace-nowrap text-xs font-bold text-body">
177+
{intlLanguagePreference
178+
? `${t("page-languages-translate-cta-title")} ${t(`language-${intlLanguagePreference.localeOption}`)}`
179+
: "Translate ethereum.org"}
180+
</p>
181+
) : (
182+
<p className="overflow-hidden text-ellipsis whitespace-nowrap text-xs font-bold text-body">
183+
{t("page-languages-translate-cta-title")}{" "}
184+
{t(`language-${locale}`)}
185+
</p>
186+
)}
187+
<p className="text-xs text-body">
188+
{t("page-languages-recruit-community")}
189+
</p>
190+
</div>
191+
<ButtonLink
192+
className="text-nowrap"
193+
href="/contributing/translation-program/"
194+
size="sm"
195+
onClick={onTranslationProgramClick}
196+
>
197+
{t("get-involved")}
198+
</ButtonLink>
199+
</div>
200+
</div>
201+
)
202+
}
203+
204+
export default MobileLanguagePicker
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"use client"
2+
3+
import { createContext, useContext, useState } from "react"
4+
5+
import type { LocaleDisplayInfo } from "@/lib/types"
6+
7+
import { SheetContent, SheetFooter, SheetHeader } from "@/components/ui/sheet"
8+
9+
import MenuFooterClient from "./MenuFooterClient"
10+
import MenuHeader from "./MenuHeader"
11+
import MobileLanguagePicker from "./MobileLanguagePicker"
12+
13+
type MobileMenuView = "menu" | "language-picker"
14+
15+
type MobileMenuContextType = {
16+
currentView: MobileMenuView
17+
setCurrentView: (view: MobileMenuView) => void
18+
}
19+
20+
const MobileMenuContext = createContext<MobileMenuContextType | undefined>(
21+
undefined
22+
)
23+
24+
export const useMobileMenu = () => {
25+
const context = useContext(MobileMenuContext)
26+
if (!context) {
27+
throw new Error("useMobileMenu must be used within MobileMenuContent")
28+
}
29+
return context
30+
}
31+
32+
type MobileMenuContentProps = {
33+
menuBody: React.ReactNode
34+
languages: LocaleDisplayInfo[]
35+
}
36+
37+
const MobileMenuContent = ({ menuBody, languages }: MobileMenuContentProps) => {
38+
const [currentView, setCurrentView] = useState<MobileMenuView>("menu")
39+
40+
return (
41+
<MobileMenuContext.Provider value={{ currentView, setCurrentView }}>
42+
<SheetContent side="left" className="flex flex-col" aria-describedby="">
43+
{/* HEADER ELEMENTS: SITE NAME, CLOSE BUTTON */}
44+
<SheetHeader>
45+
<MenuHeader />
46+
</SheetHeader>
47+
48+
{/* MAIN NAV ACCORDION CONTENTS OF MOBILE MENU */}
49+
<div className="flex-1 overflow-auto">
50+
{currentView === "menu" ? (
51+
menuBody
52+
) : (
53+
<MobileLanguagePicker languages={languages} />
54+
)}
55+
</div>
56+
57+
{/* FOOTER ELEMENTS: SEARCH, LIGHT/DARK, LANGUAGES */}
58+
{currentView === "menu" && (
59+
<SheetFooter className="h-[108px] justify-center border-t border-body-light px-4 py-0">
60+
<MenuFooterClient />
61+
</SheetFooter>
62+
)}
63+
</SheetContent>
64+
</MobileMenuContext.Provider>
65+
)
66+
}
67+
68+
export default MobileMenuContent

src/components/Nav/Mobile/index.tsx

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
1-
import {
2-
Sheet,
3-
SheetContent,
4-
SheetFooter,
5-
SheetHeader,
6-
SheetTrigger,
7-
} from "@/components/ui/sheet"
1+
import { Sheet, SheetTrigger } from "@/components/ui/sheet"
82

93
import { cn } from "@/lib/utils/cn"
104

115
import { ButtonProps } from "../../ui/buttons/Button"
126

137
import HamburgerButton from "./HamburgerButton"
148
import MenuBody from "./MenuBody"
15-
import MenuFooter from "./MenuFooter"
16-
import MenuHeader from "./MenuHeader"
9+
import MobileMenuContent from "./MobileMenuContent"
10+
11+
import { getLanguagesDisplayInfo } from "@/lib/nav/links"
1712

1813
type MobileMenuProps = ButtonProps
1914

20-
const MobileMenu = ({ className, ...props }: MobileMenuProps) => {
15+
const MobileMenu = async ({ className, ...props }: MobileMenuProps) => {
16+
const languages = await getLanguagesDisplayInfo()
17+
2118
return (
2219
<Sheet>
2320
<SheetTrigger asChild>
@@ -28,22 +25,7 @@ const MobileMenu = ({ className, ...props }: MobileMenuProps) => {
2825
{...props}
2926
/>
3027
</SheetTrigger>
31-
<SheetContent side="left" className="flex flex-col" aria-describedby="">
32-
{/* HEADER ELEMENTS: SITE NAME, CLOSE BUTTON */}
33-
<SheetHeader>
34-
<MenuHeader />
35-
</SheetHeader>
36-
37-
{/* MAIN NAV ACCORDION CONTENTS OF MOBILE MENU */}
38-
<div className="flex-1 overflow-auto">
39-
<MenuBody />
40-
</div>
41-
42-
{/* FOOTER ELEMENTS: SEARCH, LIGHT/DARK, LANGUAGES */}
43-
<SheetFooter className="h-[108px] justify-center border-t border-body-light px-4 py-0">
44-
<MenuFooter />
45-
</SheetFooter>
46-
</SheetContent>
28+
<MobileMenuContent menuBody={<MenuBody />} languages={languages} />
4729
</Sheet>
4830
)
4931
}

0 commit comments

Comments
 (0)