spotters: most-active-in sort with country/region selector, collection tracking (seen marks, progress, filters), refresh scripts score activity and report new NL entries
Deploy production / deploy (push) Has been cancelled
Deploy production / deploy (push) Has been cancelled
This commit is contained in:
1 parent
3f294df337
commit
253cd69424
58 files changed
+2151
-72
No files matched your search
@@ -1,18 +1,41 @@
|
||||
import { carOverview, PAGE_SIZE, parseFilters, searchCars } from '#lib/server/cars.js';
|
||||
import {
|
||||
areasWithData,
|
||||
groupActivity,
|
||||
progress,
|
||||
rankByActivity,
|
||||
seenByGroup
|
||||
} from '#lib/server/spotting.js';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
const filters = parseFilters(url.searchParams);
|
||||
const [{ cars, total }, overview] = await Promise.all([searchCars(filters), carOverview()]);
|
||||
const [{ cars, total }, overview, collected, seenBrands, areas, act] = await Promise.all([
|
||||
searchCars(filters),
|
||||
carOverview(),
|
||||
progress('cars'),
|
||||
seenByGroup('cars'),
|
||||
areasWithData('cars'),
|
||||
groupActivity('cars', filters.area)
|
||||
]);
|
||||
return {
|
||||
filters,
|
||||
cars,
|
||||
total,
|
||||
pageSize: PAGE_SIZE,
|
||||
collected,
|
||||
areas,
|
||||
brands: overview.brands.map(({ slug, name, models }) => ({ slug, name, models })),
|
||||
topBrands: overview.byNl
|
||||
topBrands: rankByActivity(overview.byNl, act)
|
||||
.slice(0, 16)
|
||||
.map(({ slug, name, models, nlCount, logo }) => ({ slug, name, models, nlCount, logo })),
|
||||
.map(({ id, slug, name, models, nlCount, logo }) => ({
|
||||
slug,
|
||||
name,
|
||||
models,
|
||||
nlCount,
|
||||
logo,
|
||||
seen: seenBrands.get(id) ?? 0
|
||||
})),
|
||||
stats: overview.stats,
|
||||
currentYear: new Date().getFullYear(),
|
||||
seo: {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
LOGO_NOTE,
|
||||
SORTS
|
||||
} from '#lib/cars/shared.js';
|
||||
import AreaSelect from '#lib/spotting/AreaSelect.svelte';
|
||||
import { GOT } from '#lib/spotting/areas.js';
|
||||
import SpotItem from '#lib/spotting/SpotItem.svelte';
|
||||
import SpotProgress from '#lib/spotting/SpotProgress.svelte';
|
||||
import { Input } from '#lib/components/ui/input/index.js';
|
||||
import { NativeSelect } from '#lib/components/ui/native-select/index.js';
|
||||
import Search from 'lucide-svelte/icons/search';
|
||||
@@ -26,7 +30,7 @@
|
||||
const f = $derived(data.filters);
|
||||
const pages = $derived(Math.max(1, Math.ceil(data.total / data.pageSize)));
|
||||
const filtering = $derived(
|
||||
!!(f.q || f.brand || f.fuel || f.body || f.drive || f.market || f.from || f.to)
|
||||
!!(f.q || f.brand || f.fuel || f.body || f.drive || f.market || f.from || f.to || f.got)
|
||||
);
|
||||
|
||||
let form: HTMLFormElement | undefined = $state();
|
||||
@@ -51,13 +55,14 @@
|
||||
return `/trackers/cars${qs ? `?${qs}` : ''}`;
|
||||
}
|
||||
|
||||
const quick = [
|
||||
const quick: { label: string; params: Record<string, string> }[] = [
|
||||
{ label: '📖 dex order', params: { sort: 'dex' } },
|
||||
{ label: '⚡ electric', params: { fuel: 'electric', sort: 'popular' } },
|
||||
{ label: '🍃 hybrids', params: { fuel: 'hybrid', sort: 'popular' } },
|
||||
{ label: '🏎️ coupés', params: { body: 'coupe', sort: 'popular' } },
|
||||
{ label: '💪 most powerful', params: { sort: 'power' } },
|
||||
{ label: '🐎 Mustang', params: { q: 'mustang' } }
|
||||
{ label: '🐎 Mustang', params: { q: 'mustang' } },
|
||||
{ label: '⬜ still to go', params: { got: 'no' } }
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -94,6 +99,15 @@
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="mb-8">
|
||||
<SpotProgress
|
||||
seen={data.collected.seen}
|
||||
total={data.collected.total}
|
||||
label="models collected"
|
||||
href={withParams({ got: 'no' })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if !filtering && f.page === 1}
|
||||
<section class="mb-8" aria-labelledby="brands-heading">
|
||||
<div class="mb-3 flex items-baseline justify-between gap-3">
|
||||
@@ -184,6 +198,14 @@
|
||||
{#each Object.entries(SORTS) as [key, label] (key)}<option value={key}>{label}</option
|
||||
>{/each}
|
||||
</NativeSelect>
|
||||
{#if f.sort === 'active'}<AreaSelect
|
||||
value={f.area}
|
||||
available={data.areas}
|
||||
onchange={submit}
|
||||
/>{/if}
|
||||
<NativeSelect name="got" aria-label="Collection" value={f.got} onchange={submit}>
|
||||
{#each Object.entries(GOT) as [key, label] (key)}<option value={key}>{label}</option>{/each}
|
||||
</NativeSelect>
|
||||
<noscript><button type="submit" class="pill">search</button></noscript>
|
||||
</div>
|
||||
</form>
|
||||
@@ -206,7 +228,11 @@
|
||||
{#if data.cars.length}
|
||||
<ul class="grid">
|
||||
{#each data.cars as car, i (car.dex)}
|
||||
<li><CarCard {car} current={data.currentYear} eager={i < 4} /></li>
|
||||
<li>
|
||||
<SpotItem tracker="cars" id={car.id} seen={car.seen} label={car.name}>
|
||||
<CarCard {car} current={data.currentYear} eager={i < 4} />
|
||||
</SpotItem>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
|
||||
@@ -10,11 +10,18 @@
|
||||
thumbSrcset,
|
||||
years
|
||||
} from '#lib/cars/shared.js';
|
||||
import { page } from '$app/state';
|
||||
import { parseGot } from '#lib/spotting/areas.js';
|
||||
import { countSeen, filterGot } from '#lib/spotting/seen.svelte.js';
|
||||
import SpotItem from '#lib/spotting/SpotItem.svelte';
|
||||
import SpotProgress from '#lib/spotting/SpotProgress.svelte';
|
||||
import ArrowLeft from 'lucide-svelte/icons/arrow-left';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
const b = $derived(data.brand);
|
||||
const got = $derived(parseGot(page.url.searchParams.get('got')));
|
||||
const shown = $derived(filterGot('cars', data.models, got));
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-6xl px-4 py-12 sm:px-6 sm:py-16">
|
||||
@@ -57,9 +64,21 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="mb-6">
|
||||
<SpotProgress
|
||||
seen={countSeen('cars', data.models)}
|
||||
total={data.models.length}
|
||||
label="{b.name} models collected"
|
||||
{got}
|
||||
/>
|
||||
</div>
|
||||
<ul class="grid">
|
||||
{#each data.models as car, i (car.dex)}
|
||||
<li><CarCard {car} current={data.currentYear} eager={i < 4} /></li>
|
||||
{#each shown as car, i (car.dex)}
|
||||
<li>
|
||||
<SpotItem tracker="cars" id={car.id} seen={car.seen} label={car.name}>
|
||||
<CarCard {car} current={data.currentYear} eager={i < 4} />
|
||||
</SpotItem>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p class="mt-10 text-xs text-[var(--site-muted)]">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getModel } from '#lib/server/cars.js';
|
||||
import type { carModels, carVariants } from '#lib/server/db/schema/index.js';
|
||||
import { getSeen } from '#lib/server/spotting.js';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
type Model = typeof carModels.$inferSelect;
|
||||
@@ -32,7 +33,9 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
const found = await getModel(params.brand, params.model);
|
||||
if (!found) error(404, 'No such car in the pokedex');
|
||||
const { model, brand, variants, photos, prev, next } = found;
|
||||
const seenEntry = await getSeen('cars', model.id);
|
||||
return {
|
||||
spot: { id: model.id, entry: seenEntry },
|
||||
car: publicCar(model),
|
||||
brand: { slug: brand.slug, name: brand.name, country: brand.country, logo: brand.logo },
|
||||
variants: variants.map(publicVariant),
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import ChevronLeft from 'lucide-svelte/icons/chevron-left';
|
||||
import ChevronRight from 'lucide-svelte/icons/chevron-right';
|
||||
import Shuffle from 'lucide-svelte/icons/shuffle';
|
||||
import SeenPanel from '#lib/spotting/SeenPanel.svelte';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
@@ -174,6 +175,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
<SeenPanel tracker="cars" id={data.spot.id} entry={data.spot.entry} label="this one" />
|
||||
|
||||
{#if data.photos.length}
|
||||
<Gallery photos={data.photos} name="{data.brand.name} {car.name}" wikipedia={car.wikipedia} />
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { carOverview } from '#lib/server/cars.js';
|
||||
import { seenByGroup } from '#lib/server/spotting.js';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const { byNl, stats } = await carOverview();
|
||||
const [{ byNl, stats }, seen] = await Promise.all([carOverview(), seenByGroup('cars')]);
|
||||
return {
|
||||
brands: byNl.map(({ slug, name, models, nlCount, logo }) => ({
|
||||
brands: byNl.map(({ id, slug, name, models, nlCount, logo }) => ({
|
||||
seen: seen.get(id) ?? 0,
|
||||
slug,
|
||||
name,
|
||||
models,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { isOwner } from '#lib/server/owner.js';
|
||||
import { parseSeenBody, setSeen } from '#lib/server/spotting.js';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
/** Owner-only: mark / unmark an entry as seen (collected), optionally with a date and note. */
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
if (!isOwner(event)) error(404, 'Not Found');
|
||||
const body = parseSeenBody(await event.request.json().catch(() => null));
|
||||
if (!body) error(400, 'Bad request');
|
||||
const ok = await setSeen('cars', body.id, body.seen, body.seenOn, body.note);
|
||||
if (!ok) error(404, 'No such entry');
|
||||
return json({ ok: true, ...body });
|
||||
};
|
||||
Reference in new issue
Block a user