v3: consistent tabs url across app, maxy small fixes
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s

This commit is contained in:
khannurien
2026-06-22 17:25:40 +00:00
parent dbd2e15158
commit a1b71ad0c8
22 changed files with 295 additions and 236 deletions

View File

@@ -11,7 +11,8 @@ import { useLocation } from "react-router";
import { AppHeader } from "../components/AppHeader.tsx";
import { PresenceRow } from "../components/PresenceRow.tsx";
import { FeedTabBar } from "../components/FeedTabBar.tsx";
import { type FeedTab, VALID_TABS } from "../config/feedTabs.ts";
import { type FeedTab, FEED_TABS } from "../config/feedTabs.ts";
import { useTabParam } from "../hooks/useTabParam.ts";
import { API_URL, DEFAULT_PAGE_SIZE } from "../config/api.ts";
@@ -82,20 +83,20 @@ export function Index() {
);
const mainFetchDone = useRef(false);
const searchParams = new URLSearchParams(location.search);
const rawTab = searchParams.get("tab") ?? "hot";
const tab: FeedTab = VALID_TABS.has(rawTab) ? rawTab as FeedTab : "hot";
const [tab] = useTabParam<FeedTab>(FEED_TABS, "hot");
// Web Share Target: Android share sheet navigates to /?share_url=...
const searchParams = new URLSearchParams(location.search);
const shareUrl = searchParams.get("share_url") ??
searchParams.get("share_text") ?? "";
useEffect(() => {
if (!shareUrl) return;
// Clean share params from the URL so a refresh doesn't re-open the modal
const clean = tab !== "hot" ? `?tab=${tab}` : "";
globalThis.history.replaceState({}, "", location.pathname + clean);
}, [shareUrl, tab, location.pathname]);
// Clean share params from the URL so a refresh doesn't re-open the modal.
// The active tab already lives in the pathname (e.g. /~/new), so just drop
// the query string.
globalThis.history.replaceState({}, "", location.pathname);
}, [shareUrl, location.pathname]);
// ── Main feed fetch ──

View File

@@ -70,7 +70,7 @@ export function ResetPassword() {
);
}
const handleSubmit = async (e: React.FormEvent) => {
const handleSubmit = async (e: React.SubmitEvent) => {
e.preventDefault();
if (mismatch || tooShort || !newPassword) return;

View File

@@ -10,6 +10,7 @@ import { ErrorCard } from "../components/ErrorCard.tsx";
import { useAuth } from "../hooks/useAuth.ts";
import { useWS } from "../hooks/useWS.ts";
import { useInfiniteScroll } from "../hooks/useInfiniteScroll.ts";
import { useTabParam } from "../hooks/useTabParam.ts";
import {
deserializeDump,
deserializePlaylist,
@@ -23,7 +24,8 @@ import {
} from "../model.ts";
import { DEFAULT_PAGE_SIZE, SEARCH_URL } from "../config/api.ts";
type Tab = "dumps" | "users" | "playlists";
const SEARCH_TABS = ["dumps", "users", "playlists"] as const;
type Tab = (typeof SEARCH_TABS)[number];
type SearchState =
| { status: "idle" }
@@ -44,9 +46,9 @@ type SearchState =
};
export function Search() {
const [searchParams, setSearchParams] = useSearchParams();
const [searchParams] = useSearchParams();
const q = searchParams.get("q") ?? "";
const tab = (searchParams.get("tab") ?? "dumps") as Tab;
const [tab, setTab] = useTabParam<Tab>(SEARCH_TABS, "dumps");
const { token, user } = useAuth();
const { voteCounts, myVotes, castVote, removeVote } = useWS();
@@ -159,14 +161,6 @@ export function Search() {
!state.dumps.loadingMore,
);
function setTab(tab: Tab) {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set("tab", tab);
return next;
}, { replace: true });
}
const dumpCount = state.status === "loaded" ? state.dumps.total : null;
const userCount = state.status === "loaded" ? state.users.length : null;
const playlistCount = state.status === "loaded"
@@ -188,7 +182,7 @@ export function Search() {
<main className="search-page">
{q && (
<TabBar
tabs={(["dumps", "users", "playlists"] as Tab[]).map((key) => ({
tabs={SEARCH_TABS.map((key) => ({
key,
label: tabLabel(
key,

View File

@@ -64,7 +64,7 @@ export function UserLogin() {
}
};
const handleResetRequest = async (e: React.FormEvent) => {
const handleResetRequest = async (e: React.SubmitEvent) => {
e.preventDefault();
if (!resetEmail.trim()) return;
setResetState({ status: "submitting" });

View File

@@ -54,6 +54,7 @@ import { TextEditor } from "../components/TextEditor.tsx";
import { Markdown } from "../components/Markdown.tsx";
import { ChangePasswordModal } from "../components/ChangePasswordModal.tsx";
import { TabBar } from "../components/TabBar.tsx";
import { useTabParam } from "../hooks/useTabParam.ts";
function InviteButton() {
const { authFetch } = useAuth();
@@ -146,7 +147,14 @@ type InviteTreeState =
| { status: "loaded"; entries: InviteTreeEntry[] };
type InviteTreeNode = InviteTreeEntry & { children: InviteTreeNode[] };
type ProfileTab = "dumps" | "playlists" | "followed" | "invitees" | "settings";
const PROFILE_TABS = [
"dumps",
"playlists",
"followed",
"invitees",
"settings",
] as const;
type ProfileTab = (typeof PROFILE_TABS)[number];
function buildInviteTree(
entries: InviteTreeEntry[],
@@ -193,6 +201,13 @@ export function UserPublicProfile() {
const profileUserId = state.status === "loaded" ? state.user.id : null;
const isOwnProfile = me?.id === profileUserId;
// Active tab is driven by the `/~/<tab>` URL path (linkable / refresh-safe).
// `settings` is only valid on your own profile; otherwise it falls back to "dumps".
const availableTabs: ProfileTab[] = isOwnProfile
? [...PROFILE_TABS]
: PROFILE_TABS.filter((t) => t !== "settings");
const [tab, setTab] = useTabParam<ProfileTab>(availableTabs, "dumps");
const setDumps = useCallback((fn: (prev: Dump[]) => Dump[]) => {
setState((s) =>
s.status !== "loaded"
@@ -287,7 +302,6 @@ export function UserPublicProfile() {
const [emailError, setEmailError] = useState<string | null>(null);
const prevMyVotesRef = useRef<Set<string> | null>(null);
const [tab, setTab] = useState<ProfileTab>("dumps");
const [changePasswordOpen, setChangePasswordOpen] = useState(false);
const [followedState, setFollowedState] = useState<FollowedState>(null);
const [inviteTreeState, setInviteTreeState] = useState<InviteTreeState>(null);
@@ -301,7 +315,6 @@ export function UserPublicProfile() {
if (prevUsername !== username) {
setPrevUsername(username);
setState({ status: "loading" });
setTab("dumps");
setFollowedState(null);
setInviteTreeState(null);
}