Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/components/CodeEmbed/frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface CodeBundle {
js?: string;
base?: string;
scripts?: string[];
parentOrigin?: string;
}

/*
Expand Down Expand Up @@ -51,6 +52,8 @@ ${(code.scripts?.length ?? 0) > 0 ? '' : `
<script type="text/javascript">
// Listen for p5.min.js text content and include in iframe's head as script
window.addEventListener("message", event => {
if (event.origin !== '${code.parentOrigin}') return;
if (event.source !== window.parent) return;
// Include check to prevent p5.min.js from being loaded twice
const scriptExists = !!document.getElementById("p5ScriptTagInIframe");
if (!scriptExists && event.data?.sender === '${cdnLibraryUrl}') {
Expand Down Expand Up @@ -139,7 +142,7 @@ export const CodeFrame = (props: CodeFrameProps) => {
sender: cdnLibraryUrl,
message: p5ScriptText,
},
"*",
window.location.origin,
);
} catch (e) {
console.error(`Error loading ${p5ScriptTag.src}`);
Expand All @@ -161,6 +164,7 @@ export const CodeFrame = (props: CodeFrameProps) => {
htmlBody: props.htmlBodyCode,
base: props.base,
scripts: props.scripts,
parentOrigin: window.location.origin,
}) : ""}
sandbox="allow-scripts allow-popups allow-modals allow-forms allow-same-origin"
aria-label="Code Preview"
Expand Down
61 changes: 41 additions & 20 deletions src/components/SearchProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from "preact/hooks";
import Fuse, { type FuseResult } from "fuse.js";
import { useEffect, useRef, useState } from "preact/hooks";
import Fuse from "fuse.js";
import SearchResults from "../SearchResults";
import { defaultLocale } from "@/src/i18n/const";

Expand All @@ -16,6 +16,9 @@ type SearchResult = {
description?: string;
};

type SearchIndexEntry = Omit<SearchResult, "id" | "category" | "title">;
type SearchIndexData = Record<string, Record<string, SearchIndexEntry>>;

/**
* SearchProvider this component is responsible for handling client-side search.
* It reads the search term from query params and fetches the search index for the current locale.
Expand All @@ -28,6 +31,8 @@ const SearchProvider = ({
}: SearchProviderProps) => {
const [searchTerm, setSearchTerm] = useState("");
const [results, setResults] = useState<SearchResult[]>([]);
const [searchIndexVersion, setSearchIndexVersion] = useState(0);
const fuseRef = useRef<Fuse<SearchResult> | null>(null);

// Flattens the search index data

Expand All @@ -48,16 +53,14 @@ const SearchProvider = ({
}
}, [searchTerm]);

// Fetch the search index for the current locale and search for the search term
// This effect runs whenever the search term or the current locale changes
// Fetch the search index for the current locale
useEffect(() => {
if (!currentLocale) {
console.warn("No locale provided to SearchProvider");
return;
}

if (!searchTerm) return;
const flattenData = (data: FuseResult<SearchResult>) => {
const flattenData = (data: SearchIndexData) => {
const flatData: SearchResult[] = [];
let flatId = 0;
Object.entries(data).forEach(([category, entries]) => {
Expand All @@ -82,12 +85,11 @@ const SearchProvider = ({
return flatData;
};

let flatData;

fetch(`/search-indices/${currentLocale}.json`)
const controller = new AbortController();
fetch(`/search-indices/${currentLocale}.json`, { signal: controller.signal })
.then((response) => response.json())
.then((data) => {
flatData = flattenData(data);
const flatData = flattenData(data);

const fuseOptions = {
includeScore: true,
Expand All @@ -100,18 +102,37 @@ const SearchProvider = ({
threshold: 0.3,
};

const fuse = new Fuse(flatData, fuseOptions);
fuseRef.current = new Fuse(flatData, fuseOptions);
setSearchIndexVersion((version) => version + 1);
})
.catch((error) => {
if (error.name !== "AbortError") {
console.error("Error fetching or indexing data:", error);
}
});

const searchResults = fuse
.search(searchTerm)
.map((result) => result.item);
return () => controller.abort();
}, [currentLocale]);

setResults(searchResults);
})
.catch((error) =>
console.error("Error fetching or indexing data:", error),
);
}, [searchTerm, currentLocale]);
// Search after search term updates
useEffect(() => {
if (!searchTerm) {
setResults([]);
return;
}

if (!fuseRef.current) return;

const timeoutId = window.setTimeout(() => {
const searchResults = fuseRef.current
?.search(searchTerm)
.map((result) => result.item);

setResults(searchResults || []);
}, 250);

return () => window.clearTimeout(timeoutId);
}, [searchTerm, searchIndexVersion]);

const handleSearchTermChange = (term?: string) => {
if (term !== undefined) {
Expand Down
Loading