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; selectedItem: OptionItem;
setSelectedItem: (value: OptionItem) => void; setSelectedItem: (value: OptionItem) => void;
options: Array<OptionItem>; options: Array<OptionItem>;
direction?: "up" | "down";
} }
export function Dropdown(props: DropdownProps) { export function Dropdown(props: DropdownProps) {
const { direction = "down" } = props;
return ( return (
<div className="relative my-4 max-w-[25rem]"> <div className="relative my-4 max-w-[25rem]">
<Listbox value={props.selectedItem} onChange={props.setSelectedItem}> <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"> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<Icon <Icon
icon={Icons.UP_DOWN_ARROW} 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> </span>
</Listbox.Button> </Listbox.Button>
@ -41,7 +44,9 @@ export function Dropdown(props: DropdownProps) {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" 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) => ( {props.options.map((opt) => (
<Listbox.Option <Listbox.Option
className={({ active }) => className={({ active }) =>

View file

@ -3,6 +3,7 @@ import { Trans, useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
import { Toggle } from "@/components/buttons/Toggle"; import { Toggle } from "@/components/buttons/Toggle";
import { Dropdown } from "@/components/form/Dropdown";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { SettingsCard } from "@/components/layout/SettingsCard"; import { SettingsCard } from "@/components/layout/SettingsCard";
import { Stepper } from "@/components/layout/Stepper"; import { Stepper } from "@/components/layout/Stepper";
@ -36,6 +37,7 @@ import {
import { PageTitle } from "@/pages/parts/util/PageTitle"; import { PageTitle } from "@/pages/parts/util/PageTitle";
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { Region, useRegionStore } from "@/utils/detectRegion";
import { getProxyUrls } from "@/utils/proxyUrls"; import { getProxyUrls } from "@/utils/proxyUrls";
import { Status, testFebboxToken } from "../parts/settings/SetupPart"; import { Status, testFebboxToken } from "../parts/settings/SetupPart";
@ -53,6 +55,7 @@ export function FEDAPISetup() {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const febboxToken = useAuthStore((s) => s.febboxToken); const febboxToken = useAuthStore((s) => s.febboxToken);
const setFebboxToken = useAuthStore((s) => s.setFebboxToken); const setFebboxToken = useAuthStore((s) => s.setFebboxToken);
const { region, setRegion } = useRegionStore();
const [status, setStatus] = useState<Status>("unset"); const [status, setStatus] = useState<Status>("unset");
const statusMap: Record<Status, StatusCircleProps["type"]> = { const statusMap: Record<Status, StatusCircleProps["type"]> = {
@ -61,6 +64,14 @@ export function FEDAPISetup() {
unset: "noresult", 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(() => { useEffect(() => {
const checkTokenStatus = async () => { const checkTokenStatus = async () => {
const result = await getFebboxTokenStatus(febboxToken); const result = await getFebboxTokenStatus(febboxToken);
@ -165,6 +176,21 @@ export function FEDAPISetup() {
passwordToggleable passwordToggleable
className="flex-grow" 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> </div>
{status === "error" && ( {status === "error" && (
<p className="text-type-danger mt-4"> <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 { Button } from "@/components/buttons/Button";
import { Toggle } from "@/components/buttons/Toggle"; import { Toggle } from "@/components/buttons/Toggle";
import { Dropdown } from "@/components/form/Dropdown";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { SettingsCard } from "@/components/layout/SettingsCard"; import { SettingsCard } from "@/components/layout/SettingsCard";
import { import {
@ -26,6 +27,7 @@ import {
} from "@/pages/parts/settings/SetupPart"; } from "@/pages/parts/settings/SetupPart";
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { Region, useRegionStore } from "@/utils/detectRegion";
interface ProxyEditProps { interface ProxyEditProps {
proxyUrls: string[] | null; proxyUrls: string[] | null;
@ -229,6 +231,7 @@ async function getFebboxTokenStatus(febboxToken: string | null) {
function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) { function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [showVideo, setShowVideo] = useState(false); const [showVideo, setShowVideo] = useState(false);
const { region, setRegion } = useRegionStore();
const [status, setStatus] = useState<Status>("unset"); const [status, setStatus] = useState<Status>("unset");
const statusMap: Record<Status, StatusCircleProps["type"]> = { const statusMap: Record<Status, StatusCircleProps["type"]> = {
@ -237,6 +240,14 @@ function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) {
unset: "noresult", 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(() => { useEffect(() => {
const checkTokenStatus = async () => { const checkTokenStatus = async () => {
const result = await getFebboxTokenStatus(febboxToken); const result = await getFebboxTokenStatus(febboxToken);
@ -337,6 +348,19 @@ function FebboxTokenEdit({ febboxToken, setFebboxToken }: FebboxTokenProps) {
passwordToggleable passwordToggleable
className="flex-grow" 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> </div>
{status === "error" && ( {status === "error" && (
<p className="text-type-danger mt-4"> <p className="text-type-danger mt-4">

View file

@ -12,18 +12,22 @@ export type Region =
interface RegionStore { interface RegionStore {
region: Region | null; region: Region | null;
lastChecked: number | 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 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>()( export const useRegionStore = create<RegionStore>()(
persist( persist(
(set) => ({ (set) => ({
region: null, region: null,
lastChecked: 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", name: "__MW::region",
@ -48,10 +52,16 @@ function determineRegion(data: {
export async function detectRegion(): Promise<Region> { export async function detectRegion(): Promise<Region> {
const store = useRegionStore.getState(); 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 ( if (
store.region && store.region &&
store.lastChecked && store.lastChecked &&
Date.now() - store.lastChecked < THIRTY_MINUTES_MS Date.now() - store.lastChecked < ONE_DAY_MS
) { ) {
return store.region; return store.region;
} }
@ -61,7 +71,9 @@ export async function detectRegion(): Promise<Region> {
const data = await response.json(); const data = await response.json();
const detectedRegion = determineRegion(data); 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; return detectedRegion;
} catch (error) { } catch (error) {
console.warn("Failed to detect region:", error); console.warn("Failed to detect region:", error);