Skip to content

Commit 553b11a

Browse files
nikoshellegeakman
andauthored
Improve search. (#1111)
Close #1052. --------- Co-authored-by: Ege Akman <[email protected]>
1 parent 46af5fb commit 553b11a

18 files changed

+707
-297
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@astrojs/sitemap": "^3.3.0",
1919
"@astrojs/tailwind": "^5.1.4",
2020
"@fontsource-variable/inter": "^5.1.1",
21+
"@fortawesome/fontawesome-free": "^6.7.2",
2122
"@tailwindcss/typography": "^0.5.16",
2223
"@types/react": "^19.1.0",
2324
"@types/react-dom": "^19.1.1",

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/BaseHead.astro

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
---
2-
// Import the global.css file here so that it is included on
3-
// all pages through the use of the <BaseHead /> component.
4-
import "../styles/global.css";
5-
import "@fontsource-variable/inter";
62
73
interface Props {
84
title: string;
@@ -58,8 +54,3 @@ const { title, description, image = "/social-card.png" } = Astro.props;
5854
is:inline
5955
data-domain="ep2025.europython.eu"
6056
src="https://plausible.io/js/script.js"></script>
61-
62-
<script
63-
is:inline
64-
src="https://kit.fontawesome.com/14a4971ab3.js"
65-
crossorigin="anonymous"></script>

src/components/footer.astro renamed to src/components/Footer.astro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const buildTimestamp = import.meta.env.TIMESTAMP;
88
const gitVersion = import.meta.env.GIT_VERSION;
99
---
1010

11+
<div class="mt-auto">
1112
<Fullbleed className="bg-primary text-white">
1213
<footer
1314
class="max-w-4xl lg:max-w-6xl mx-auto py-16 lg:grid grid-cols-2 px-6 gap-60"
@@ -106,3 +107,4 @@ const gitVersion = import.meta.env.GIT_VERSION;
106107
</article>
107108
</footer>
108109
</Fullbleed>
110+
</div>

src/components/header/header.astro renamed to src/components/Header.astro

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
import { promises as fs } from "fs";
3-
import { NavItems } from "../nav-items";
4-
import HeaderActions from "./header-actions.astro";
5-
import HeaderLogo from "./header-logo.astro";
3+
import { NavItems } from "@components/nav-items";
4+
import HeaderActions from "@components/header/header-actions.astro";
5+
import HeaderLogo from "@components/header/header-logo.astro";
66
7-
const links = JSON.parse(await fs.readFile("./src/data/links.json", "utf-8"));
7+
import links from "../data/links.json";
88
---
99

1010
<header class="p-6 flex items-center justify-between relative z-40">
@@ -27,7 +27,7 @@ const links = JSON.parse(await fs.readFile("./src/data/links.json", "utf-8"));
2727
<div
2828
class="fixed bg-body-background top-0 left-0 w-screen h-screen overflow-scroll hidden peer-checked:block xl:peer-checked:hidden z-50 p-6"
2929
>
30-
<div class="flex items-center">
30+
<div class="flex items-center justify-between">
3131
<HeaderLogo />
3232
<HeaderActions mobile />
3333
</div>

src/components/Modal.astro

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
---
2+
import Button from '@ui/Button.astro';
3+
const { id = 'modal', open = false, closeOnOutsideClick = false } = Astro.props;
4+
---
5+
<div
6+
id={id}
7+
class={`fixed inset-0 z-50 flex items-center justify-center transition-opacity duration-300 ${
8+
open ? 'opacity-100 visible bg-black/50' : 'opacity-0 invisible'
9+
}`}
10+
data-modal-wrapper
11+
data-close-on-outside-click={closeOnOutsideClick.toString()}
12+
>
13+
<div
14+
class="relative bg-white dark:bg-gray-800 p-6 lg:rounded-2xl lg:shadow-xl lg:max-w-4xl lg:mx-4 h-full lg:h-[80vh] w-full lg:w-[70vw] max-h-full overflow-y-auto"
15+
data-modal-content
16+
>
17+
<Button
18+
id="search-close"
19+
clear
20+
icon="close"
21+
iconSize="fa-xl"
22+
data-close-modal
23+
class="w-[3em] h-[3em] absolute top-4 right-4 text-primary hover:text-black "
24+
/>
25+
<slot />
26+
</div>
27+
</div>
28+
<script is:inline>
29+
document.addEventListener('DOMContentLoaded', () => {
30+
// Function to toggle body scroll
31+
const toggleBodyScroll = (disable) => {
32+
if (disable) {
33+
// Save the current scroll position
34+
const scrollY = window.scrollY;
35+
document.body.style.position = 'fixed';
36+
document.body.style.top = `-${scrollY}px`;
37+
document.body.style.width = '100%';
38+
document.body.dataset.scrollPosition = scrollY.toString();
39+
} else {
40+
// Restore scroll position
41+
const scrollY = parseInt(document.body.dataset.scrollPosition || '0');
42+
document.body.style.position = '';
43+
document.body.style.top = '';
44+
document.body.style.width = '';
45+
window.scrollTo(0, scrollY);
46+
delete document.body.dataset.scrollPosition;
47+
}
48+
};
49+
50+
// Function to open modal
51+
const openModal = (modal) => {
52+
if (modal) {
53+
// Disable body scroll
54+
toggleBodyScroll(true);
55+
// Show modal
56+
modal.classList.remove('opacity-0', 'invisible');
57+
modal.classList.add('opacity-100', 'visible');
58+
}
59+
};
60+
61+
// Function to close modal
62+
const closeModal = (modal) => {
63+
if (modal) {
64+
// Enable body scroll
65+
toggleBodyScroll(false);
66+
// Hide modal
67+
modal.classList.remove('opacity-100', 'visible');
68+
modal.classList.add('opacity-0', 'invisible');
69+
}
70+
};
71+
72+
// Open modal buttons
73+
document.querySelectorAll('[data-open-modal]').forEach(button => {
74+
button.addEventListener('click', () => {
75+
const modal = document.getElementById(button.dataset.openModal);
76+
openModal(modal);
77+
});
78+
});
79+
80+
// Close modal buttons
81+
document.querySelectorAll('[data-close-modal]').forEach(button => {
82+
button.addEventListener('click', () => {
83+
const modal = button.closest('[data-modal-wrapper]');
84+
closeModal(modal);
85+
});
86+
});
87+
88+
// Close when clicking outside the modal content - only if enabled
89+
document.querySelectorAll('[data-modal-wrapper]').forEach(modal => {
90+
const closeOnOutsideClick = modal.dataset.closeOnOutsideClick === 'true';
91+
92+
if (closeOnOutsideClick) {
93+
modal.addEventListener('click', (event) => {
94+
// Check if the click was on the wrapper but not on the content
95+
if (event.target === modal) {
96+
closeModal(modal);
97+
}
98+
});
99+
}
100+
101+
// Initialize any modal that's set to open by default
102+
if (modal.classList.contains('opacity-100') && modal.classList.contains('visible')) {
103+
toggleBodyScroll(true);
104+
}
105+
});
106+
107+
// Handle escape key to close all modals
108+
document.addEventListener('keydown', (event) => {
109+
if (event.key === 'Escape') {
110+
const visibleModals = document.querySelectorAll('[data-modal-wrapper].visible, [data-modal-wrapper].opacity-100');
111+
visibleModals.forEach(modal => closeModal(modal));
112+
}
113+
});
114+
});
115+
</script>

src/components/Search.astro

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
---
2+
import SearchComponent from "astro-pagefind/components/Search";
3+
import Button from "@ui/Button.astro";
4+
import Modal from "@components/Modal.astro";
5+
6+
---
7+
8+
9+
<Modal id="modal-search" closeOnOutsideClick=true>
10+
<h2 class="text-xl font-bold mb-2 py-2">Search</h2>
11+
<SearchComponent
12+
id="search"
13+
className="pagefind-ui"
14+
uiOptions={{
15+
showImages: false,
16+
translations: {
17+
zero_results: "Couldn't find [SEARCH_TERM]",
18+
},
19+
}}
20+
/>
21+
</div>
22+
</div>
23+
</Modal>
24+
25+
26+
<script>
27+
document.addEventListener("DOMContentLoaded", function () {
28+
const searchContainer = document.querySelector(".pagefind-ui") as HTMLElement | null;
29+
const searchInput = searchContainer?.querySelector("input") as HTMLInputElement | null;
30+
const searchButton = document.getElementById("searchButton") as HTMLElement | null;
31+
const openModal = document.querySelector('[data-open-modal="modal-search"]') as HTMLElement;
32+
const closeButton = document.querySelector('#search-close') as HTMLElement | null;
33+
34+
let selectedIndex = -1;
35+
36+
function openSearch() {
37+
console.log("open");
38+
openModal.click();
39+
if (searchInput) {
40+
searchInput.value = "Tips";
41+
const inputEvent = new Event("input", { bubbles: true });
42+
searchInput.dispatchEvent(inputEvent);
43+
setTimeout(() => {
44+
searchInput.value = "";
45+
searchInput.placeholder = "Tips for You";
46+
}, 100);
47+
searchInput.focus();
48+
}
49+
}
50+
51+
function closeSearch() {
52+
closeButton?.click();
53+
}
54+
55+
function updateSelection() {
56+
const results = searchContainer?.querySelectorAll(".pagefind-ui__result");
57+
if (!results) return;
58+
59+
results.forEach((result, index) => {
60+
if (result instanceof HTMLElement) {
61+
if (index === selectedIndex) {
62+
result.classList.add("selected");
63+
result.scrollIntoView({ block: "nearest", behavior: "smooth" });
64+
} else {
65+
result.classList.remove("selected");
66+
}
67+
}
68+
});
69+
}
70+
71+
function makeResultsClickable() {
72+
const results = searchContainer?.querySelectorAll(".pagefind-ui__result");
73+
if (!results) return;
74+
75+
results.forEach((result) => {
76+
const link = result.querySelector(".pagefind-ui__result-link") as HTMLAnchorElement | null;
77+
if (link && !result.hasAttribute("data-clickable")) {
78+
if (result instanceof HTMLElement) {
79+
result.style.cursor = "pointer";
80+
result.addEventListener("click", (e) => {
81+
if (!(e.target instanceof HTMLElement) || !e.target.closest("a")) {
82+
link.click();
83+
}
84+
});
85+
result.setAttribute("data-clickable", "true");
86+
}
87+
}
88+
});
89+
}
90+
91+
if (searchButton) {
92+
searchButton.addEventListener("click", openSearch);
93+
}
94+
95+
document.addEventListener("keydown", function (event) {
96+
if (!searchContainer || !searchInput) return;
97+
98+
const results = searchContainer.querySelectorAll(".pagefind-ui__result");
99+
100+
if (event.ctrlKey && event.key === "k") {
101+
event.preventDefault();
102+
openSearch();
103+
} else if (event.key === "Escape") {
104+
closeSearch();
105+
} else if (document.activeElement === searchInput) {
106+
if (event.key === "ArrowDown") {
107+
event.preventDefault();
108+
selectedIndex = (selectedIndex + 1) % results.length;
109+
updateSelection();
110+
} else if (event.key === "ArrowUp") {
111+
event.preventDefault();
112+
selectedIndex = (selectedIndex - 1 + results.length) % results.length;
113+
updateSelection();
114+
} else if (event.key === "Enter") {
115+
event.preventDefault();
116+
if (results.length > 0) {
117+
const indexToOpen = selectedIndex === -1 ? 0 : selectedIndex;
118+
const link = results[indexToOpen]?.querySelector("a") as HTMLAnchorElement | null;
119+
link?.click();
120+
}
121+
}
122+
}
123+
});
124+
125+
const resultsObserver = new MutationObserver(() => {
126+
const results = searchContainer?.querySelectorAll(".pagefind-ui__result");
127+
if (results && results.length > 0) {
128+
selectedIndex = 0;
129+
updateSelection();
130+
makeResultsClickable();
131+
}
132+
});
133+
134+
if (searchContainer) {
135+
resultsObserver.observe(searchContainer, {
136+
childList: true,
137+
subtree: true,
138+
});
139+
}
140+
141+
closeButton?.addEventListener("click", closeSearch);
142+
});
143+
</script>

0 commit comments

Comments
 (0)