fixing platform switcher bugs

This commit is contained in:
2025-12-22 09:03:32 -05:00
parent 34e915f904
commit 061d737c6c
2 changed files with 89 additions and 48 deletions
+32 -14
View File
@@ -21,7 +21,10 @@ import Pagination from "@/components/parts/Pagination";
import PlatformSwitcher from "@/components/parts/PlatformSwitcher"; import PlatformSwitcher from "@/components/parts/PlatformSwitcher";
import type { CategoryId } from "@/types/gunbuilder"; import type { CategoryId } from "@/types/gunbuilder";
import { PART_ROLE_TO_CATEGORY, normalizePartRole } from "@/lib/catalogMappings"; import {
PART_ROLE_TO_CATEGORY,
normalizePartRole,
} from "@/lib/catalogMappings";
type ViewMode = "card" | "list"; type ViewMode = "card" | "list";
type SortOption = "relevance" | "price-asc" | "price-desc" | "brand-asc"; type SortOption = "relevance" | "price-asc" | "price-desc" | "brand-asc";
@@ -196,7 +199,8 @@ export default function PartsBrowseClient(props: {
); );
const priceBounds = useMemo(() => { const priceBounds = useMemo(() => {
if (!parts.length) return { min: null as number | null, max: null as number | null }; if (!parts.length)
return { min: null as number | null, max: null as number | null };
return { return {
min: Math.min(...parts.map((p) => p.price)), min: Math.min(...parts.map((p) => p.price)),
max: Math.max(...parts.map((p) => p.price)), max: Math.max(...parts.map((p) => p.price)),
@@ -208,11 +212,13 @@ export default function PartsBrowseClient(props: {
const minBound = priceBounds.min; const minBound = priceBounds.min;
const maxBound = priceBounds.max; const maxBound = priceBounds.max;
if (minBound == null || maxBound == null) return; if (minBound == null || maxBound == null) return;
setPriceRange((prev) => { setPriceRange((prev) => {
const nextMin = prev.min == null ? minBound : Math.max(minBound, prev.min); const nextMin =
const nextMax = prev.max == null ? maxBound : Math.min(maxBound, prev.max); prev.min == null ? minBound : Math.max(minBound, prev.min);
const nextMax =
prev.max == null ? maxBound : Math.min(maxBound, prev.max);
// Ensure min never exceeds max (in case user typed weird values) // Ensure min never exceeds max (in case user typed weird values)
return { return {
min: Math.min(nextMin, nextMax), min: Math.min(nextMin, nextMax),
@@ -232,15 +238,13 @@ export default function PartsBrowseClient(props: {
if (brandFilter.length) if (brandFilter.length)
result = result.filter((p) => brandFilter.includes(p.brand)); result = result.filter((p) => brandFilter.includes(p.brand));
if (inStockOnly) if (inStockOnly) result = result.filter((p) => p.inStock ?? true);
result = result.filter((p) => p.inStock ?? true);
const q = searchQuery.toLowerCase().trim(); const q = searchQuery.toLowerCase().trim();
if (q) if (q)
result = result.filter( result = result.filter(
(p) => (p) =>
p.name.toLowerCase().includes(q) || p.name.toLowerCase().includes(q) || p.brand.toLowerCase().includes(q)
p.brand.toLowerCase().includes(q)
); );
switch (sortBy) { switch (sortBy) {
@@ -318,12 +322,22 @@ export default function PartsBrowseClient(props: {
</p> </p>
{!isBuilderMode && ( {!isBuilderMode && (
<div className="mt-3"> <div className="mt-4 flex flex-wrap items-center gap-3">
<PlatformSwitcher <PlatformSwitcher
currentPlatform={effectivePlatform} currentPlatform={effectivePlatform}
partRole={partRole} partRole={partRole}
preserveQuery preserveQuery
mode="browse"
/> />
<Link
href={`/builder?platform=${encodeURIComponent(
effectivePlatform
)}`}
className="inline-flex items-center rounded-md bg-amber-400 px-3 py-2 text-xs font-semibold text-black hover:bg-amber-300 transition-colors"
>
Start New Build
</Link>
</div> </div>
)} )}
</div> </div>
@@ -331,7 +345,9 @@ export default function PartsBrowseClient(props: {
{isBuilderMode && ( {isBuilderMode && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link <Link
href={`/builder?platform=${encodeURIComponent(effectivePlatform)}`} href={`/builder?platform=${encodeURIComponent(
effectivePlatform
)}`}
className="rounded-md border border-zinc-700 bg-zinc-900/70 px-3 py-2 text-xs font-semibold hover:bg-zinc-800" className="rounded-md border border-zinc-700 bg-zinc-900/70 px-3 py-2 text-xs font-semibold hover:bg-zinc-800"
> >
Back to Build Back to Build
@@ -399,7 +415,9 @@ export default function PartsBrowseClient(props: {
)} )}
{loading ? ( {loading ? (
<p className="py-8 text-center text-sm text-zinc-500">Loading</p> <p className="py-8 text-center text-sm text-zinc-500">
Loading
</p>
) : error ? ( ) : error ? (
<p className="py-8 text-center text-sm text-red-400">{error}</p> <p className="py-8 text-center text-sm text-red-400">{error}</p>
) : filteredParts.length === 0 ? ( ) : filteredParts.length === 0 ? (
@@ -434,4 +452,4 @@ export default function PartsBrowseClient(props: {
</div> </div>
</main> </main>
); );
} }
+57 -34
View File
@@ -1,52 +1,75 @@
"use client"; "use client";
import { useMemo } from "react"; import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useSearchParams } from "next/navigation";
const PLATFORMS = ["AR-15", "AR-10", "AR-9"] as const;
type Platform = (typeof PLATFORMS)[number];
export default function PlatformSwitcher(props: { export default function PlatformSwitcher(props: {
currentPlatform: string; currentPlatform: string;
partRole: string; partRole: string;
preserveQuery?: boolean; // keep sort/page/etc (but NOT platform) /**
* If true, preserve *all* existing query params (except platform will be overwritten).
* Useful when you want to keep things like ?from=builder or future filters.
*/
preserveQuery?: boolean;
/**
* Force route behavior.
* - "browse": /parts/[role]?platform=...
* - "detail": /parts/p/[platform]/[role]
* Default: auto based on current pathname
*/
mode?: "browse" | "detail";
}) { }) {
const { currentPlatform, partRole, preserveQuery = true } = props;
const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const queryString = useMemo(() => { const inferredMode: "browse" | "detail" = pathname.startsWith("/parts/p/")
if (!preserveQuery) return ""; ? "detail"
: "browse";
// Copy params and DROP legacy platform param so it cant conflict with the path const mode = props.mode ?? inferredMode;
const sp = new URLSearchParams(searchParams?.toString());
sp.delete("platform");
const q = sp.toString(); const buildHref = (nextPlatform: Platform) => {
return q ? `?${q}` : ""; if (mode === "detail") {
}, [searchParams, preserveQuery]); return `/parts/p/${encodeURIComponent(nextPlatform)}/${encodeURIComponent(
props.partRole
)}`;
}
function go(platform: string) { // browse mode => keep on /parts/[role] and just change the query param
router.replace( const qp = props.preserveQuery
`/parts/p/${encodeURIComponent(platform)}/${encodeURIComponent(partRole)}${queryString}` ? new URLSearchParams(searchParams.toString())
); : new URLSearchParams();
}
qp.set("platform", nextPlatform);
return {
pathname: `/parts/${encodeURIComponent(props.partRole)}`,
query: Object.fromEntries(qp.entries()),
} as const;
};
return ( return (
<div className="flex items-center gap-2"> <div className="inline-flex items-center gap-1 rounded-md border border-zinc-800 bg-zinc-950/60 p-1">
<span className="text-xs uppercase tracking-[0.14em] text-zinc-500">Platform</span> {PLATFORMS.map((p) => {
<select const active = p === props.currentPlatform;
value={currentPlatform} return (
onChange={(e) => go(e.target.value)} <Link
className="rounded-md border border-zinc-700 bg-zinc-950/80 px-2 py-1 text-xs text-zinc-100 outline-none focus:border-amber-400" key={p}
> href={buildHref(p)}
<option value="AR-15">AR-15</option> className={`px-3 py-1.5 text-xs font-medium rounded transition-colors ${
<option value="AR-10">AR-10</option> active
<option value="AR-9">AR-9</option> ? "bg-zinc-800 text-zinc-50"
<option value="AK-47">AK-47</option> : "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-900/60"
</select> }`}
>
{/* debug only */} {p}
<span className="ml-2 text-[0.7rem] text-zinc-500">{pathname}</span> </Link>
);
})}
</div> </div>
); );
} }