Skip to content

Commit

Permalink
POC
Browse files Browse the repository at this point in the history
  • Loading branch information
shakyShane committed Feb 6, 2025
1 parent 1f07543 commit d140264
Show file tree
Hide file tree
Showing 17 changed files with 474 additions and 80 deletions.
70 changes: 70 additions & 0 deletions special-pages/pages/history/HistoryProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { h, createContext } from 'preact';
import { useContext } from 'preact/hooks';
import { useSearchContext } from './app/components/SearchForm.js';

Check failure on line 3 in special-pages/pages/history/HistoryProvider.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

'useSearchContext' is defined but never used
import { useSignalEffect } from '@preact/signals';
import { paramsToQuery } from './history.service.js';
import { OVERSCAN_AMOUNT } from './app/constants.js';

// Create the context
const HistoryServiceContext = createContext({
service: /** @type {import("./history.service").HistoryService} */ ({}),
initial: /** @type {import("./history.service").ServiceData} */ ({}),
});

// Provider component
/**
* Provides a context for the history service, allowing dependent components to access it.
*
* @param {Object} props - The properties object for the HistoryServiceProvider component.
* @param {import("./history.service").HistoryService} props.service - The history service instance to be provided through the context.
* @param {import("./history.service").ServiceData} props.initial - The history service instance to be provided through the context.
* @param {import("preact").ComponentChild} props.children - The child components that will consume the history service context.
*/
export function HistoryServiceProvider({ service, initial, children }) {
useSignalEffect(() => {
let seen = 0;
// Add a listener for the 'search-commit' event
window.addEventListener('search-commit', (/** @type {CustomEvent<{params: URLSearchParams}>} */ event) => {
const detail = event.detail;
if (seen > 0) {
if (detail && detail.params instanceof URLSearchParams) {
const asQuery = paramsToQuery(detail.params);
service.trigger(asQuery);
} else {
console.error('missing detail.params from search-commit event');
}
}
seen += 1;
});

// Cleanup the event listener on unmount
return () => {
window.removeEventListener('search-commit', this);
};
});

useSignalEffect(() => {
function handler(/** @type {CustomEvent<{start: number, end: number}>} */ event) {
if (!service.query.data) throw new Error('unreachable');
const { end } = event.detail;
const memory = service.query.data.results;
if (memory.length - end < OVERSCAN_AMOUNT) {
service.requestMore();
}
}
window.addEventListener('range-change', handler);
return () => {
window.removeEventListener('range-change', handler);
};
});
return <HistoryServiceContext.Provider value={{ service, initial }}>{children}</HistoryServiceContext.Provider>;
}

// Hook for consuming the context
export function useHistory() {
const context = useContext(HistoryServiceContext);
if (!context) {
throw new Error('useHistoryService must be used within a HistoryServiceProvider');
}
return context;
}
43 changes: 36 additions & 7 deletions special-pages/pages/history/app/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import styles from './App.module.css';
import { useTypedTranslation } from '../types.js';
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
import { Header } from './Header.js';
import { useSignal } from '@preact/signals';
import { batch, useSignal, useSignalEffect } from '@preact/signals';
import { Results } from './Results.js';
import { useRef } from 'preact/hooks';
import { SearchProvider } from './SearchForm.js';
import { useHistory } from '../../HistoryProvider.js';
import { generateHeights } from '../utils.js';

/**
* @typedef {object} Results
Expand All @@ -16,21 +19,47 @@ import { useRef } from 'preact/hooks';
export function App() {
const { t } = useTypedTranslation();
const { isDarkMode } = useEnv();
const containerRef = useRef(null);
const containerRef = useRef(/** @type {HTMLElement|null} */ (null));
const { initial, service } = useHistory();

const results = useSignal({
items: [],
heights: [],
items: initial.query.results,
heights: generateHeights(initial.query.results),
});

const term = useSignal(initial.query.info.term);

useSignalEffect(() => {
const unsub = service.onResults((data) => {
batch(() => {
term.value = data.info.term;
results.value = {
items: data.results,
heights: generateHeights(data.results),
};
});
});
return () => {
unsub();
};
});

useSignalEffect(() => {
term.subscribe((t) => {
containerRef.current?.scrollTo(0, 0);
});
});

return (
<div class={styles.layout} data-theme={isDarkMode ? 'dark' : 'light'}>
<header class={styles.header}>
<Header results={results} />
<Header />
</header>
<aside class={styles.aside}>
<h1 class={styles.pageTitle}>History</h1>
</aside>
<main class={styles.main} ref={containerRef} data-main-scroller>
<Results results={results} containerRef={containerRef} />
<main class={styles.main} ref={containerRef} data-main-scroller data-term={term}>
<Results results={results} term={term} containerRef={containerRef} />
</main>
</div>
);
Expand Down
65 changes: 6 additions & 59 deletions special-pages/pages/history/app/components/Header.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,15 @@
import styles from './Header.module.css';
import { Fire } from '../icons/Fire.js';
import { h } from 'preact';
import { useMessaging, useTypedTranslation } from '../types.js';
import { Cross } from '../icons/Cross.js';
import { useSignalEffect } from '@preact/signals';
import { generateHeights } from '../utils.js';
import { useComputed } from '@preact/signals';
import { SearchForm, useSearchContext } from './SearchForm.js';

/**
* @param {object} props
* @param {import("@preact/signals").Signal<import("./App.jsx").Results>} props.results
*/
export function Header({ results }) {
const { t } = useTypedTranslation();
const historyPage = useMessaging();
useSignalEffect(() => {
historyPage
.query({ term: '', limit: 150, offset: 0 })
// eslint-disable-next-line promise/prefer-await-to-then
.then((next) => {
const combined = results.value.items.concat(next.value);
results.value = {
items: combined,
heights: generateHeights(combined),
};
})
// eslint-disable-next-line promise/prefer-await-to-then
.catch((e) => {
console.log('did catch...', e);
});
let fetching = false;
function handler(/** @type {CustomEvent<{start: number, end: number}>} */ event) {
const { end } = event.detail;
if (results.value.items.length - end < 20) {
if (fetching) return;
fetching = true;
historyPage
.query({ term: '', limit: 150, offset: results.value.items.length })
// eslint-disable-next-line promise/prefer-await-to-then
.then((next) => {
const combined = results.value.items.concat(next.value);
results.value = {
items: combined,
heights: generateHeights(combined),
};
fetching = false;
})
// eslint-disable-next-line promise/prefer-await-to-then
.catch((e) => {
console.log('did catch...', e);
});
}
}
window.addEventListener('range-change', handler);
return () => {
window.removeEventListener('range-change', handler);
};
});
export function Header() {
const search = useSearchContext();
const term = useComputed(() => search.value.term);
return (
<div class={styles.root}>
<div class={styles.controls}>
Expand All @@ -69,14 +23,7 @@ export function Header({ results }) {
</button>
</div>
<div class={styles.search}>
<form
action=""
onSubmit={(e) => {
e.preventDefault();
}}
>
<input type="search" placeholder={t('search')} class={styles.searchInput} name="term" />
</form>
<SearchForm term={term} />
</div>
</div>
);
Expand Down
11 changes: 9 additions & 2 deletions special-pages/pages/history/app/components/Results.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { h } from 'preact';
import { VirtualizedHistoryList } from './VirtualizedHistoryList.js';
import { OVERSCAN_AMOUNT } from '../constants.js';

/**
* @param {object} props
* @param {import("@preact/signals").Signal<import("./App.jsx").Results>} props.results
* @param {import("@preact/signals").Signal<string>} props.term
* @param {any} props.containerRef
*/
export function Results({ results, containerRef }) {
export function Results({ results, term, containerRef }) {
return (
<div>
<VirtualizedHistoryList items={results.value.items} heights={results.value.heights} containerRef={containerRef} />
<VirtualizedHistoryList
items={results.value.items}
heights={results.value.heights}
containerRef={containerRef}
overscan={OVERSCAN_AMOUNT}
/>
</div>
);
}
102 changes: 102 additions & 0 deletions special-pages/pages/history/app/components/SearchForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import styles from './Header.module.css';
import { createContext, h } from 'preact';
import { useTypedTranslation } from '../types.js';
import { signal, useSignal, useSignalEffect } from '@preact/signals';
import { useContext } from 'preact/hooks';

/**
* @param {object} props
* @param {import("@preact/signals").Signal<string>} props.term
*/
export function SearchForm({ term }) {
const { t } = useTypedTranslation();
// const onInput = (event) => (term.value = event.currentTarget.value);
return (
<form role="search">
<label>
<span class="sr-only">{t('search_your_history')}</span>
<input type="search" placeholder={t('search')} className={styles.searchInput} name="term" value={term} />
</label>
</form>
);
}

const SearchContext = createContext(signal({ term: '' }));

/**
* A custom hook to access the SearchContext.
*
* @returns {import('@preact/signals').Signal<{ term: string }>} The current search context value.
*/
export function useSearchContext() {
return useContext(SearchContext);
}

/**
* A provider component that sets up the search context for its children. Allows access to and updates of the search term within the context.
*
* @param {Object} props - The props object for the component.
* @param {import("preact").ComponentChild} props.children - The child components wrapped within the provider.
* @param {string} [props.term=''] - The initial search term for the context.
*/
export function SearchProvider({ children, term = '' }) {
const searchState = useSignal({ term });
useSignalEffect(() => {
const controller = new AbortController();

// @ts-expect-error - later
window._accept = (v) => {
searchState.value = { ...searchState.value, term: v };
};

document.addEventListener(
'submit',
(e) => {
e.preventDefault();
console.log('re-issue search plz', [searchState.value.term]);
},
{ signal: controller.signal },
);

document.addEventListener(
'input',
(e) => {
if (e.target instanceof HTMLInputElement) {
searchState.value = { ...searchState.value, term: e.target.value };
}
},
{ signal: controller.signal },
);

return () => {
controller.abort();
};
});

useSignalEffect(() => {
let timer;
const sub = searchState.subscribe((v) => {
clearTimeout(timer);
timer = setTimeout(() => {
const url = new URL(window.location.href);
url.searchParams.delete('q');
if (searchState.value.term) {
console.log('update URL with term:', searchState.value.term);
url.searchParams.set('q', searchState.value.term);
window.history.replaceState(null, '', url.toString());
} else {
const url = new URL(window.location.href);
window.history.replaceState(null, '', url.toString());
}
window.dispatchEvent(new CustomEvent('search-commit', { detail: { params: new URLSearchParams(url.searchParams) } }));
}, 500);
});

return () => {
sub();
clearTimeout(timer);
};
});

return <SearchContext.Provider value={searchState}>{children}</SearchContext.Provider>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ export const VirtualizedHistoryList = memo(
* @param {object} props
* @param {import('../../types/history').HistoryItem[]} props.items
* @param {number[]} props.heights
* @param {number} props.overscan
* @param {any} props.containerRef
*/
function VirtualizedPreact({ items, heights, containerRef }) {
function VirtualizedPreact({ items, heights, overscan, containerRef }) {
const totalHeight = heights.reduce((acc, item) => acc + item, 0);
return (
<div class={styles.container} style={{ height: totalHeight + 'px' }}>
<Inner items={items} heights={heights} containerRef={containerRef} />
<Inner items={items} heights={heights} containerRef={containerRef} overscan={overscan} />
</div>
);
},
Expand All @@ -26,10 +27,11 @@ const Inner = memo(
* @param {object} props
* @param {import("../../types/history").HistoryItem[]} props.items
* @param {number[]} props.heights
* @param {number} props.overscan
* @param {any} props.containerRef
*/
function Inner({ items, heights, containerRef }) {
const { start, end } = useVisibleRows(items, heights, containerRef);
function Inner({ items, heights, overscan, containerRef }) {
const { start, end } = useVisibleRows(items, heights, containerRef, overscan);
const subset = items.slice(start, end + 1);
return (
<ul>
Expand Down
1 change: 1 addition & 0 deletions special-pages/pages/history/app/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const OVERSCAN_AMOUNT = 5;
Loading

0 comments on commit d140264

Please sign in to comment.