Cache search results in shared links — no re-searching on open
When sharing a search, the actual results are now saved alongside the search config. Opening a shared link shows the cached results instantly instead of re-running the search against all stores. - Add results_json column to shared_searches (migration 006) - Frontend sends current results array when creating a share - Share page loads cached results directly, shows date saved - "Search Live Prices" button available to re-run with fresh data - Falls back to live search if no cached results exist Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,7 @@ export function saveSharedSearch(data: {
|
||||
excludedStores?: string;
|
||||
sortBy?: string;
|
||||
sortAsc?: boolean;
|
||||
results?: any[];
|
||||
}) {
|
||||
return api<any>('/api/share', { method: 'POST', body: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
excludedStores: excludedStores.size > 0 ? [...excludedStores].join(',') : undefined,
|
||||
sortBy,
|
||||
sortAsc,
|
||||
results,
|
||||
});
|
||||
const shareUrl = `${window.location.origin}/share/${shared.share_id}`;
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
|
||||
@@ -34,8 +34,20 @@
|
||||
excludedStores = new Set(shared.excluded_stores.split(','));
|
||||
}
|
||||
|
||||
// Run the search with saved store selection
|
||||
// Use cached results if available, otherwise re-search
|
||||
if (shared.results && shared.results.length > 0) {
|
||||
results = shared.results;
|
||||
searchDone = true;
|
||||
meta = {
|
||||
totalResults: shared.results.length,
|
||||
storeCount: new Set(shared.results.map((r) => r.storeName)).size,
|
||||
duration: 0,
|
||||
errors: [],
|
||||
};
|
||||
loading = false;
|
||||
} else {
|
||||
doSearch(shared.store_ids || undefined);
|
||||
}
|
||||
} catch {
|
||||
loadError = 'Shared search not found or has expired.';
|
||||
loading = false;
|
||||
@@ -156,7 +168,12 @@
|
||||
</h1>
|
||||
{#if meta}
|
||||
<span class="text-2xs text-text-tertiary">
|
||||
({meta.totalResults} results from {meta.storeCount} stores in {meta.duration}ms)
|
||||
({meta.totalResults} results from {meta.storeCount} stores{#if meta.duration > 0} in {meta.duration}ms{/if})
|
||||
</span>
|
||||
{/if}
|
||||
{#if sharedConfig?.created_at}
|
||||
<span class="text-2xs text-text-tertiary">
|
||||
· saved {new Date(sharedConfig.created_at + 'Z').toLocaleDateString()}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -164,7 +181,9 @@
|
||||
{#if shareMessage}
|
||||
<span class="text-2xs text-emerald-400">{shareMessage}</span>
|
||||
{/if}
|
||||
<a href="/results?q={encodeURIComponent(query)}" class="btn-secondary text-xs py-1">Search Again</a>
|
||||
<a href="/results?q={encodeURIComponent(query)}" class="btn-secondary text-xs py-1">
|
||||
Search Live Prices
|
||||
</a>
|
||||
<a href="/" class="btn-secondary text-xs py-1">New Search</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
1
src/server/db/migrations/006_add_shared_results.sql
Normal file
1
src/server/db/migrations/006_add_shared_results.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE shared_searches ADD COLUMN results_json TEXT;
|
||||
@@ -11,6 +11,7 @@ export interface SharedSearch {
|
||||
excluded_stores: string | null;
|
||||
sort_by: string;
|
||||
sort_asc: number;
|
||||
results_json: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -25,13 +26,14 @@ export function createSharedSearch(data: {
|
||||
excludedStores?: string;
|
||||
sortBy?: string;
|
||||
sortAsc?: boolean;
|
||||
results?: any[];
|
||||
}): SharedSearch {
|
||||
const db = getDatabase();
|
||||
const shareId = generateShareId();
|
||||
|
||||
db.run(`
|
||||
INSERT INTO shared_searches (share_id, query, store_ids, filter_text, excluded_stores, sort_by, sort_asc)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO shared_searches (share_id, query, store_ids, filter_text, excluded_stores, sort_by, sort_asc, results_json)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
shareId,
|
||||
data.query,
|
||||
@@ -40,6 +42,7 @@ export function createSharedSearch(data: {
|
||||
data.excludedStores || null,
|
||||
data.sortBy || 'price',
|
||||
data.sortAsc !== false ? 1 : 0,
|
||||
data.results ? JSON.stringify(data.results) : null,
|
||||
]);
|
||||
|
||||
saveDatabase();
|
||||
@@ -47,5 +50,7 @@ export function createSharedSearch(data: {
|
||||
}
|
||||
|
||||
export function getSharedSearch(shareId: string): SharedSearch | undefined {
|
||||
return queryOne('SELECT * FROM shared_searches WHERE share_id = ?', [shareId]);
|
||||
const row = queryOne('SELECT * FROM shared_searches WHERE share_id = ?', [shareId]);
|
||||
if (!row) return undefined;
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export const shareRoutes: FastifyPluginAsync = async (app) => {
|
||||
excludedStores: { type: 'string' },
|
||||
sortBy: { type: 'string' },
|
||||
sortAsc: { type: 'boolean' },
|
||||
results: { type: 'array' },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -36,6 +37,13 @@ export const shareRoutes: FastifyPluginAsync = async (app) => {
|
||||
app.get<{ Params: { id: string } }>('/share/:id', async (request, reply) => {
|
||||
const shared = getSharedSearch(request.params.id);
|
||||
if (!shared) return reply.code(404).send({ error: 'Shared search not found' });
|
||||
return shared;
|
||||
|
||||
// Parse cached results JSON
|
||||
let results = null;
|
||||
if (shared.results_json) {
|
||||
try { results = JSON.parse(shared.results_json); } catch { /* ignore */ }
|
||||
}
|
||||
|
||||
return { ...shared, results, results_json: undefined };
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user