|
| 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 |
0 commit comments