Skip to content

Commit 427d05b

Browse files
Copilotdanmunzaih
authored
fix(search): autocomplete suggestions and Enter key now deliver results (#59)
* fix(search): make suggestions and Enter key trigger results - onSelect with item.url: use SPA loadContent() instead of window.location.href to avoid a full-page reload - onSelect without item.url: call performSearch() so the selected suggestion actually highlights matches in the current chapter - Add onSubmit handler so pressing Enter in the autocomplete input triggers performSearch() Co-authored-by: danmunz <282860+danmunz@users.noreply.github.com> * fix(search): configure local algolia env & replace production urls with local origins --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: danmunz <282860+danmunz@users.noreply.github.com> Co-authored-by: aih <arihershowitz@gmail.com>
1 parent 4421899 commit 427d05b

File tree

3 files changed

+88
-9
lines changed

3 files changed

+88
-9
lines changed

CompendiumUI/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
node_modules
22
dist
3+
.env
4+
.env.*
5+

CompendiumUI/script.ts

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ interface AlgoliaResult {
4646
hits: AlgoliaItem[];
4747
}
4848

49+
export function replaceUrlOrigin(originalUrl: string, newOrigin: string): string {
50+
try {
51+
const url = new URL(originalUrl);
52+
const newOriginUrl = new URL(newOrigin);
53+
url.protocol = newOriginUrl.protocol;
54+
url.host = newOriginUrl.host;
55+
url.port = newOriginUrl.port;
56+
return url.toString();
57+
} catch {
58+
// If it's invalid or a relative path, return as is.
59+
return originalUrl;
60+
}
61+
}
62+
63+
4964
interface AlgoliaItem {
5065
objectID: string;
5166
title: string;
@@ -1850,34 +1865,45 @@ document.addEventListener('DOMContentLoaded', () => {
18501865
},
18511866
// --- Define What Happens On Select ---
18521867
onSelect({ item, setQuery, setIsOpen }) {
1853-
// Example: Navigate to a product page or log the selection
18541868
console.log('Selected:', item);
1855-
// If you have product URLs in your index (e.g., item.url)
1869+
setIsOpen(false);
18561870
if (item.url) {
18571871
// Security: Validate URL origin before navigation to prevent open redirect
18581872
try {
1859-
const targetUrl = new URL(item.url, window.location.origin);
1860-
// Only navigate to URLs on the same origin
1873+
const targetUrl = new URL(replaceUrlOrigin(item.url, window.location.origin), window.location.origin);
1874+
// Only navigate to URLs on the same origin (this will now pass locally)
18611875
if (targetUrl.origin === window.location.origin) {
1862-
window.location.href = targetUrl.toString();
1876+
// Use SPA navigation instead of a full-page reload
1877+
const pathname = targetUrl.pathname;
1878+
const potentialFilename = pathname.startsWith('/') ? pathname.substring(1) : pathname;
1879+
1880+
const targetHash = targetUrl.hash ? targetUrl.hash.substring(1) : null;
1881+
if (potentialFilename) {
1882+
loadContent(potentialFilename, { updateHistory: true, targetHash: targetHash, isInitialLoad: false });
1883+
}
18631884
} else {
18641885
console.warn('External URL in search result blocked for security:', item.url);
18651886
}
18661887
} catch (error) {
18671888
console.error('Invalid URL in search result:', item.url, error);
18681889
}
18691890
} else {
1870-
// Or maybe fill the input with the selected item's name
1871-
setQuery(item.title);
1872-
setIsOpen(false); // Close the dropdown
1873-
// You might want to trigger a full search here if needed
1891+
// No URL available — highlight the item title in the current chapter
1892+
const searchTerm = item.sectionTitle || item.title;
1893+
setQuery(searchTerm);
1894+
performSearch(searchTerm);
18741895
}
18751896
},
18761897
},
18771898
// You can add more sources here (e.g., searching multiple indices)
18781899
];
18791900
},
18801901

1902+
// Trigger in-page highlighting when the user submits the query (e.g. presses Enter)
1903+
onSubmit({ state }) {
1904+
performSearch(state.query);
1905+
},
1906+
18811907
// Optional: Customize Autocomplete appearance and behavior further
18821908
// See Autocomplete.js documentation for more options
18831909
// Example: Detached mode (dropdown appears separate from input)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { replaceUrlOrigin } from './script';
3+
4+
describe('replaceUrlOrigin', () => {
5+
it('should replace the origin of an absolute URL', () => {
6+
const original = 'https://copyright-compendium.vercel.app/demo/page.html#section-1';
7+
const targetOrigin = 'http://localhost:5174';
8+
const expected = 'http://localhost:5174/demo/page.html#section-1';
9+
10+
expect(replaceUrlOrigin(original, targetOrigin)).toBe(expected);
11+
});
12+
13+
it('should handle different target protocols (http vs https)', () => {
14+
const original = 'http://example.com/path';
15+
const targetOrigin = 'https://compendium.adhocteam.us';
16+
const expected = 'https://compendium.adhocteam.us/path';
17+
18+
expect(replaceUrlOrigin(original, targetOrigin)).toBe(expected);
19+
});
20+
21+
it('should handle root paths without trailing slashes', () => {
22+
const original = 'https://copyright-compendium.vercel.app';
23+
const targetOrigin = 'http://127.0.0.1:3000';
24+
const expected = 'http://127.0.0.1:3000/';
25+
26+
expect(replaceUrlOrigin(original, targetOrigin)).toBe(expected);
27+
});
28+
29+
it('should keep query parameters intact', () => {
30+
const original = 'https://copyright-compendium.vercel.app/search?q=test&page=2';
31+
const targetOrigin = 'http://localhost:8080';
32+
const expected = 'http://localhost:8080/search?q=test&page=2';
33+
34+
expect(replaceUrlOrigin(original, targetOrigin)).toBe(expected);
35+
});
36+
37+
it('should return original string if it is a relative path (invalid URL constructor)', () => {
38+
const original = '/relative/path.html';
39+
const targetOrigin = 'http://localhost:5174';
40+
41+
expect(replaceUrlOrigin(original, targetOrigin)).toBe(original);
42+
});
43+
44+
it('should return original string if targetOrigin is invalid', () => {
45+
const original = 'https://example.com/path';
46+
const targetOrigin = 'invalid-origin';
47+
48+
expect(replaceUrlOrigin(original, targetOrigin)).toBe(original);
49+
});
50+
});

0 commit comments

Comments
 (0)