fixing platform switcher bugs
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 can’t 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user