Filters
Four selects bind signals. Type, destination, and limit changes fetch
/api/filters-results (results only). A region change resets
$destination and fetches /api/filters, which patches both the
controls (fresh destination options) and the results in one SSE response.
Find an experience
23 matches · showing first 5
- Old Town ghost walkCulture · Edinburgh, Scotland
- Festival fringe crawlCulture · Edinburgh, Scotland
- West End food tourFood · Glasgow, Scotland
- Barras weekend marketMarket · Glasgow, Scotland
- Munro bagging weekendOutdoors · Highlands, Scotland
The controls
---
/** Filter selects; a region change re-renders this block with fresh destinations. */
import { regions, types } from '../data/experiences';
interface Props {
region: string;
}
const { region } = Astro.props;
const destinations = region ? (regions[region] ?? []) : [];
---
<div id="filter-controls" class="filter-grid">
<div>
<label for="f-type">Experience type</label>
<select
id="f-type"
data-bind:type
data-on:change="@get('/api/filters-results')"
>
<option value="">Any type</option>
{types.map((t) => <option value={t}>{t}</option>)}
</select>
</div>
<div>
<label for="f-region">Region</label>
<select
id="f-region"
data-bind:region
data-on:change="$destination = ''; @get('/api/filters')"
>
<option value="">Any region</option>
{Object.keys(regions).map((r) => <option value={r}>{r}</option>)}
</select>
</div>
<div>
<label for="f-destination">Destination</label>
<select
id="f-destination"
data-bind:destination
data-on:change="@get('/api/filters-results')"
disabled={destinations.length === 0}
>
<option value="">
{destinations.length ? 'Any destination' : 'Pick a region first'}
</option>
{destinations.map((d) => <option value={d}>{d}</option>)}
</select>
</div>
<div>
<label for="f-limit">Show</label>
<select
id="f-limit"
data-bind:limit
data-on:change="@get('/api/filters-results')"
>
<option value="5">5 results</option>
<option value="10">10 results</option>
<option value="20">20 results</option>
</select>
</div>
</div>The results
---
import { experiences } from '../data/experiences';
interface Props {
type: string;
region: string;
destination: string;
limit: number;
}
const { type, region, destination, limit } = Astro.props;
const matches = experiences.filter(
(e) =>
(!type || e.type === type) &&
(!region || e.region === region) &&
(!destination || e.destination === destination),
);
const shown = matches.slice(0, limit);
---
<div id="filter-results">
<p class="hint">
{matches.length} match{matches.length === 1 ? '' : 'es'}
{matches.length > shown.length && ` · showing first ${shown.length}`}
</p>
<ul class="entry-list">
{
shown.map((e) => (
<li>
<strong>{e.title}</strong>
<span class="hint">
{e.type} · {e.destination}, {e.region}
</span>
</li>
))
}
{matches.length === 0 && <li class="hint">Nothing matches those filters.</li>}
</ul>
</div>The region endpoint
import {
patchElements,
readSignals,
SignalsValidationError,
sse,
} from '@wrux/astro-datastar/server';
import type { APIRoute } from 'astro';
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { z } from 'astro/zod';
import FilterControls from '../../components/FilterControls.astro';
import FilterResults from '../../components/FilterResults.astro';
export const prerender = false;
export const filterSignals = z.object({
type: z.string().catch(''),
region: z.string().catch(''),
destination: z.string().catch(''),
limit: z.coerce.number().int().catch(5),
});
// Region changed: destination options are stale, so re-render the controls
// and the results in one response.
export const GET: APIRoute = async ({ request }) => {
let signals: z.infer<typeof filterSignals>;
try {
signals = await readSignals(request, filterSignals);
} catch (err) {
if (err instanceof SignalsValidationError) return err.response();
throw err;
}
const container = await AstroContainer.create();
const [controls, results] = await Promise.all([
container.renderToString(FilterControls, {
props: { region: signals.region },
}),
container.renderToString(FilterResults, { props: signals }),
]);
return sse(patchElements(controls), patchElements(results));
};The results endpoint
import {
html,
readSignals,
SignalsValidationError,
} from '@wrux/astro-datastar/server';
import type { APIRoute } from 'astro';
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import type { z } from 'astro/zod';
import FilterResults from '../../components/FilterResults.astro';
import { filterSignals } from './filters';
export const prerender = false;
export const GET: APIRoute = async ({ request }) => {
let signals: z.infer<typeof filterSignals>;
try {
signals = await readSignals(request, filterSignals);
} catch (err) {
if (err instanceof SignalsValidationError) return err.response();
throw err;
}
const container = await AstroContainer.create();
return html(
await container.renderToString(FilterResults, { props: signals }),
);
};