add manual region picker

This commit is contained in:
Pas 2025-04-17 18:53:40 -06:00
parent 8262e5ba12
commit 35a90264e6
4 changed files with 74 additions and 7 deletions

View file

@ -13,9 +13,12 @@ interface DropdownProps {
selectedItem: OptionItem;
setSelectedItem: (value: OptionItem) => void;
options: Array<OptionItem>;
direction?: "up" | "down";
}
export function Dropdown(props: DropdownProps) {
const { direction = "down" } = props;
return (
<div className="relative my-4 max-w-[25rem]">
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}>
@ -31,7 +34,7 @@ export function Dropdown(props: DropdownProps) {
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<Icon
icon={Icons.UP_DOWN_ARROW}
className="transform transition-transform text-xl text-dropdown-secondary"
className={`transform transition-transform text-xl text-dropdown-secondary ${direction === "up" ? "rotate-180" : ""}`}
/>
</span>
</Listbox.Button>
@ -41,7 +44,9 @@ export function Dropdown(props: DropdownProps) {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute left-0 right-0 top-10 z-[100] mt-4 max-h-60 overflow-auto rounded-md bg-dropdown-background py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-background-secondary scrollbar-thumb-type-secondary focus:outline-none sm:top-10">
<Listbox.Options
className={`absolute left-0 right-0 z-[100] mt-4 max-h-60 overflow-auto rounded-md bg-dropdown-background py-1 text-white shadow-lg ring-1 ring-black ring-opacity-5 scrollbar-thin scrollbar-track-background-secondary scrollbar-thumb-type-secondary focus:outline-none ${direction === "up" ? "bottom-full mb-4" : "top-full"}`}
>
{props.options.map((opt) => (
<Listbox.Option
className={({ active }) =>

View file

@ -3,6 +3,7 @@ import { Trans, useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { Toggle } from "@/components/buttons/Toggle";
import { Dropdown } from "@/components/form/Dropdown";
import { Icon, Icons } from "@/components/Icon";
import { SettingsCard } from "@/components/layout/SettingsCard";
import { Stepper } from "@/components/layout/Stepper";
@ -36,6 +37,7 @@ import {
import { PageTitle } from "@/pages/parts/util/PageTitle";
import { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth";
import { Region, useRegionStore } from "@/utils/detectRegion";
import { getProxyUrls } from "@/utils/proxyUrls";
import { Status, testFebboxToken } from "../parts/settings/SetupPart";
@ -53,6 +55,7 @@ export function FEDAPISetup() {
const [isExpanded, setIsExpanded] = useState(false);
const febboxToken = useAuthStore((s) => s.febboxToken);
const setFebboxToken = useAuthStore((s) => s.setFebboxToken);
const { region, setRegion } = useRegionStore();
const [status, setStatus] = useState<Status>("unset");
const statusMap: Record<Status, StatusCircleProps["type"]> = {
@ -61,6 +64,14 @@ export function FEDAPISetup() {
unset: "noresult",
};
const regionOptions = [
{ id: "us-east", name: "US East" },
{ id: "us-west", name: "US West" },
{ id: "south-america", name: "South America" },
{ id: "asia", name: "Asia" },
{ id: "europe", name: "Europe" },
];
useEffect(() => {
const checkTokenStatus = async () => {
const result = await getFebboxTokenStatus(febboxToken);
@ -165,6 +176,21 @@ export function FEDAPISetup() {
passwordToggleable
className="flex-grow"
/>
<div className="ml-4 w-40">
<Dropdown
options={regionOptions}
selectedItem={{
id: region || "us-east",
name:
regionOptions.find((r) => r.id === region)?.name ||
"US East",
}}
setSelectedItem={(item) =>
setRegion(item.id as Region, true)
}
direction="up"
/>
</div>
</div>
{status === "error" && (
<p className="text-type-danger mt-4">

View file

@ -9,6 +9,7 @@ import { Trans, useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { Toggle } from "@/components/buttons/Toggle";
import { Dropdown } from "@/components/form/Dropdown";
import { Icon, Icons } from "@/components/Icon";
import { SettingsCard } from "@/components/layout/SettingsCard";
import {
@ -26,6 +27,7 @@ import {
} from "@/pages/parts/settings/SetupPart";
import { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth";
import { Region, useRegionStore } from "@/utils/detectRegion";
interface ProxyEditProps {
proxyUrls: string[] | null;
@ -229,6 +231,7 @@ async function getFebboxTokenStatus(febboxToken: string | null) {
function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) {
const { t } = useTranslation();
const [showVideo, setShowVideo] = useState(false);
const { region, setRegion } = useRegionStore();
const [status, setStatus] = useState<Status>("unset");
const statusMap: Record<Status, StatusCircleProps["type"]> = {
@ -237,6 +240,14 @@ function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) {
unset: "noresult",
};
const regionOptions = [
{ id: "us-east", name: "US East" },
{ id: "us-west", name: "US West" },
{ id: "south-america", name: "South America" },
{ id: "asia", name: "Asia" },
{ id: "europe", name: "Europe" },
];
useEffect(() => {
const checkTokenStatus = async () => {
const result = await getFebboxTokenStatus(febboxToken);
@ -337,6 +348,19 @@ function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) {
passwordToggleable
className="flex-grow"
/>
<div className="ml-4 w-40">
<Dropdown
options={regionOptions}
selectedItem={{
id: region || "us-east",
name:
regionOptions.find((r) => r.id === region)?.name ||
"US East",
}}
setSelectedItem={(item) => setRegion(item.id as Region, true)}
direction="up"
/>
</div>
</div>
{status === "error" && (
<p className="text-type-danger mt-4">

View file

@ -12,18 +12,22 @@ export type Region =
interface RegionStore {
region: Region | null;
lastChecked: number | null;
setRegion: (region: Region) => void;
userPicked: boolean;
setRegion: (region: Region, userPicked?: boolean) => void;
}
// const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000;
const THIRTY_MINUTES_MS = 30 * 60 * 1000;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
// const THIRTY_MINUTES_MS = 30 * 60 * 1000;
export const useRegionStore = create<RegionStore>()(
persist(
(set) => ({
region: null,
lastChecked: null,
setRegion: (region) => set({ region, lastChecked: Date.now() }),
userPicked: false,
setRegion: (region, userPicked = false) =>
set({ region, lastChecked: Date.now(), userPicked }),
}),
{
name: "__MW::region",
@ -48,10 +52,16 @@ function determineRegion(data: {
export async function detectRegion(): Promise<Region> {
const store = useRegionStore.getState();
// If user picked a region, always return that
if (store.userPicked && store.region) {
return store.region;
}
// If we have a recent detection, return that
if (
store.region &&
store.lastChecked &&
Date.now() - store.lastChecked < THIRTY_MINUTES_MS
Date.now() - store.lastChecked < ONE_DAY_MS
) {
return store.region;
}
@ -61,7 +71,9 @@ export async function detectRegion(): Promise<Region> {
const data = await response.json();
const detectedRegion = determineRegion(data);
store.setRegion(detectedRegion); // Persist the detected region
if (!store.userPicked) {
store.setRegion(detectedRegion); // Only update if not user picked
}
return detectedRegion;
} catch (error) {
console.warn("Failed to detect region:", error);