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
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s
This commit is contained in:
@@ -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 ──
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user