prices: fold groceries and CS skins into one price tracker (/trackers/prices) with tabs
Deploy production / deploy (push) Successful in 1m30s

- tab registry in #lib/price-trackers (add a tab = route + entry + icon)
- groceries -> /trackers/prices/groceries (owner-only), CS prices -> /trackers/prices/cs (public)
- old /trackers/groceries/* and /projects/cs-prices/* redirect (301 GET, 308 POST) with query
- nav: one prices entry replaces groceries and skins; cyan price-tag icon
This commit is contained in:
stan committed 2026-10-27 10:38:10 +00:00
1 parent c175c25615
commit 81b0c51672
29 files changed
+163 -28

No files matched your search

+1
View File
@@ -32,6 +32,7 @@ const staticPaths = [
'/trackers/books',
'/trackers/ai',
'/trackers/radio',
'/trackers/prices/cs',
'/trackers/spotting',
'/trackers/cars',
'/trackers/cars/brands',
+14
View File
@@ -0,0 +1,14 @@
<script lang="ts">
// One price tracker, a tab per thing (#lib/price-trackers). Each tab keeps its own page,
// loader and data; this only adds the tab bar. Private tabs are only listed for me.
import { page } from '$app/state';
import Tag from 'lucide-svelte/icons/tag';
import SpottingSwitcher from '#lib/components/SpottingSwitcher.svelte';
import { visiblePriceTabs } from '#lib/price-trackers.js';
let { children } = $props();
const tabs = $derived(visiblePriceTabs(Boolean(page.data.isOwner)));
</script>
<SpottingSwitcher items={tabs} home="/trackers/prices" label="prices" homeIcon={Tag} />
{@render children()}
@@ -0,0 +1,9 @@
import { redirect } from '@sveltejs/kit';
import { isOwner } from '#lib/server/owner.js';
import { defaultPriceTab } from '#lib/price-trackers.js';
import type { PageServerLoad } from './$types';
// /trackers/prices has no page of its own: open the first tab this visitor can see.
export const load: PageServerLoad = (event) => {
redirect(307, defaultPriceTab(isOwner(event)).href);
};
@@ -28,7 +28,7 @@
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- local, non-reactive query builder
const params = new URLSearchParams({ q: browseQuery, kind: browseKind, sort: browseSort });
params.set('offset', String(offset));
const response = await fetch(`/projects/cs-prices/api/skins?${params}`, { signal });
const response = await fetch(`/trackers/prices/cs/api/skins?${params}`, { signal });
if (!response.ok) throw new Error(`Search failed (${response.status})`);
return (await response.json()).skins as Hit[];
}
@@ -203,7 +203,7 @@
searchError = '';
try {
const response = await fetch(
`/projects/cs-prices/api/search?dimension=${encodeURIComponent(d)}&q=${encodeURIComponent(q)}`,
`/trackers/prices/cs/api/search?dimension=${encodeURIComponent(d)}&q=${encodeURIComponent(q)}`,
{ signal: controller.signal }
);
if (!response.ok) throw new Error(`Search failed (${response.status})`);
@@ -225,7 +225,7 @@
searchError = '';
try {
const response = await fetch(
`/projects/cs-prices/api/search?dimension=${encodeURIComponent(dimension)}&q=${encodeURIComponent(query)}&limit=1000`
`/trackers/prices/cs/api/search?dimension=${encodeURIComponent(dimension)}&q=${encodeURIComponent(query)}&limit=1000`
);
if (!response.ok) throw new Error(`Search failed (${response.status})`);
const found = (await response.json()).results as Result[];
@@ -262,7 +262,7 @@
loading = true;
error = '';
try {
const response = await fetch('/projects/cs-prices/api/series', {
const response = await fetch('/trackers/prices/cs/api/series', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
@@ -357,7 +357,7 @@
{#each hits as hit (hit.slug)}
<a
class="hit"
href={`/projects/cs-prices/${hit.slug}`}
href={`/trackers/prices/cs/${hit.slug}`}
style:--rarity={hit.rarity_color ?? 'transparent'}
>
{#if hit.image}<img
@@ -164,7 +164,7 @@
</svelte:head>
<div class="cs-item">
<a class="back" href="/projects/cs-prices">← CS prices</a>
<a class="back" href="/trackers/prices/cs">← CS prices</a>
<header class="head">
{#if image}<img src={image} alt="" width="160" height="120" loading="eager" />{/if}
<div class="title">
@@ -254,7 +254,7 @@
// ---- merge / move picker ------------------------------------------------------------------
async function findGroups(q: string): Promise<Hit[]> {
if (!q.trim()) return [];
const res = await fetch(`/trackers/groceries/search?q=${encodeURIComponent(q.trim())}`);
const res = await fetch(`/trackers/prices/groceries/search?q=${encodeURIComponent(q.trim())}`);
return res.ok ? ((await res.json()) as Hit[]) : [];
}
function openPicker(p: NonNullable<typeof picker>) {
@@ -487,7 +487,7 @@
<dd>{fmtNum(data.stats.promos)}</dd>
</div>
</dl>
<a href="/trackers/groceries/stats" class="stats-link">
<a href="/trackers/prices/groceries/stats" class="stats-link">
<ChartColumn size={16} aria-hidden="true" class="text-violet-500" /> stats
</a>
</header>
@@ -621,7 +621,7 @@
· page {data.page} of {fmtNum(pages)}{/if}</span
>
{#if data.filters.q || data.filters.category || data.filters.store || data.filters.favorites || data.filters.promo || data.filters.multi}
<a href="/trackers/groceries" class="clear" data-sveltekit-reset="false"
<a href="/trackers/prices/groceries" class="clear" data-sveltekit-reset="false"
><X size={12} />Clear</a
>
{/if}
@@ -588,7 +588,7 @@
<div class="wrap mx-auto max-w-6xl px-4 pt-10 pb-28 sm:px-6 sm:pt-14">
<header class="mb-5 flex flex-wrap items-end justify-between gap-3">
<div>
<a href="/trackers/groceries" class="back"><ArrowLeft size={14} /> groceries</a>
<a href="/trackers/prices/groceries" class="back"><ArrowLeft size={14} /> groceries</a>
<h1 class="text-4xl font-semibold tracking-tight sm:text-5xl">groceries stats</h1>
<p class="mt-2 flex flex-wrap items-center gap-1.5 text-sm text-[var(--site-muted)]">
<Lock size={14} aria-hidden="true" />