Skip to content

Commit 3570067

Browse files
committed
history: documenting the data model
1 parent 30215b3 commit 3570067

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2185
-277
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { h, createContext } from 'preact';
2+
import { useContext } from 'preact/hooks';
3+
import { useSignalEffect } from '@preact/signals';
4+
import { paramsToQuery } from './history.service.js';
5+
import { OVERSCAN_AMOUNT } from './constants.js';
6+
7+
// Create the context
8+
const HistoryServiceContext = createContext({
9+
service: /** @type {import("./history.service").HistoryService} */ ({}),
10+
initial: /** @type {import("./history.service").ServiceData} */ ({}),
11+
});
12+
13+
// Provider component
14+
/**
15+
* Provides a context for the history service, allowing dependent components to access it.
16+
*
17+
* @param {Object} props - The properties object for the HistoryServiceProvider component.
18+
* @param {import("./history.service").HistoryService} props.service - The history service instance to be provided through the context.
19+
* @param {import("./history.service").ServiceData} props.initial - The history service instance to be provided through the context.
20+
* @param {import("preact").ComponentChild} props.children - The child components that will consume the history service context.
21+
*/
22+
export function HistoryServiceProvider({ service, initial, children }) {
23+
useSignalEffect(() => {
24+
// Add a listener for the 'search-commit' event
25+
window.addEventListener('search-commit', (/** @type {CustomEvent<{params: URLSearchParams}>} */ event) => {
26+
const detail = event.detail;
27+
if (detail && detail.params instanceof URLSearchParams) {
28+
const asQuery = paramsToQuery(detail.params);
29+
service.trigger(asQuery);
30+
} else {
31+
console.error('missing detail.params from search-commit event');
32+
}
33+
});
34+
35+
// Cleanup the event listener on unmount
36+
return () => {
37+
window.removeEventListener('search-commit', this);
38+
};
39+
});
40+
41+
useSignalEffect(() => {
42+
function handler(/** @type {CustomEvent<{start: number, end: number}>} */ event) {
43+
if (!service.query.data) throw new Error('unreachable');
44+
const { end } = event.detail;
45+
const memory = service.query.data.results;
46+
if (memory.length - end < OVERSCAN_AMOUNT) {
47+
service.requestMore();
48+
}
49+
}
50+
window.addEventListener('range-change', handler);
51+
return () => {
52+
window.removeEventListener('range-change', handler);
53+
};
54+
});
55+
56+
useSignalEffect(() => {
57+
function handler(/** @type {MouseEvent} */ event) {
58+
if (!(event.target instanceof Element)) return;
59+
const btn = /** @type {HTMLButtonElement|null} */ (event.target.closest('button'));
60+
if (btn?.dataset.titleMenu) {
61+
event.stopImmediatePropagation();
62+
event.preventDefault();
63+
return confirm(`todo: title menu for ${btn.dataset.titleMenu}`);
64+
}
65+
if (btn?.dataset.rowMenu) {
66+
event.stopImmediatePropagation();
67+
event.preventDefault();
68+
return confirm(`todo: row menu for ${btn.dataset.rowMenu}`);
69+
}
70+
if (btn?.dataset.deleteRange) {
71+
event.stopImmediatePropagation();
72+
event.preventDefault();
73+
return confirm(`todo: delete range for ${btn.dataset.deleteRange}`);
74+
}
75+
if (btn?.dataset.deleteAll) {
76+
event.stopImmediatePropagation();
77+
event.preventDefault();
78+
return confirm(`todo: delete all`);
79+
}
80+
return null;
81+
}
82+
document.addEventListener('click', handler);
83+
return () => {
84+
document.removeEventListener('click', handler);
85+
};
86+
});
87+
return <HistoryServiceContext.Provider value={{ service, initial }}>{children}</HistoryServiceContext.Provider>;
88+
}
89+
90+
// Hook for consuming the context
91+
export function useHistory() {
92+
const context = useContext(HistoryServiceContext);
93+
if (!context) {
94+
throw new Error('useHistoryService must be used within a HistoryServiceProvider');
95+
}
96+
return context;
97+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export class Settings {
2+
/**
3+
* @param {object} params
4+
* @param {{name: 'macos' | 'windows'}} [params.platform]
5+
* @param {number} [params.typingDebounce=500] how long to debounce typing in the search field
6+
*/
7+
constructor({ platform = { name: 'macos' }, typingDebounce = 500 }) {
8+
this.platform = platform;
9+
this.typingDebounce = typingDebounce;
10+
}
11+
12+
withPlatformName(name) {
13+
/** @type {ImportMeta['platform'][]} */
14+
const valid = ['windows', 'macos'];
15+
if (valid.includes(/** @type {any} */ (name))) {
16+
return new Settings({
17+
...this,
18+
platform: { name },
19+
});
20+
}
21+
return this;
22+
}
23+
24+
/**
25+
* @param {null|undefined|number|string} value
26+
*/
27+
withDebounce(value) {
28+
if (!value) return this;
29+
const input = String(value).trim();
30+
if (input.match(/^\d+$/)) {
31+
return new Settings({
32+
...this,
33+
typingDebounce: parseInt(input, 10),
34+
});
35+
}
36+
return this;
37+
}
38+
}

special-pages/pages/history/app/components/App.jsx

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,65 @@
11
import { Fragment, h } from 'preact';
22
import styles from './App.module.css';
3-
import { useTypedTranslation } from '../types.js';
43
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
54
import { Header } from './Header.js';
6-
import { useSignal } from '@preact/signals';
5+
import { batch, useSignal, useSignalEffect } from '@preact/signals';
76
import { Results } from './Results.js';
7+
import { useRef } from 'preact/hooks';
8+
import { useHistory } from '../HistoryProvider.js';
9+
import { generateHeights } from '../utils.js';
10+
import { Sidebar } from './Sidebar.js';
11+
12+
/**
13+
* @typedef {object} Results
14+
* @property {import('../../types/history').HistoryItem[]} items
15+
* @property {number[]} heights
16+
*/
817

918
export function App() {
10-
const { t } = useTypedTranslation();
1119
const { isDarkMode } = useEnv();
20+
const containerRef = useRef(/** @type {HTMLElement|null} */ (null));
21+
const { initial, service } = useHistory();
22+
1223
const results = useSignal({
13-
info: {
14-
finished: true,
15-
term: '',
16-
},
17-
value: [],
24+
items: initial.query.results,
25+
heights: generateHeights(initial.query.results),
26+
});
27+
28+
const term = useSignal('term' in initial.query.info.query ? initial.query.info.query.term : '');
29+
30+
useSignalEffect(() => {
31+
const unsub = service.onResults((data) => {
32+
batch(() => {
33+
if ('term' in data.info.query && data.info.query.term !== null) {
34+
term.value = data.info.query.term;
35+
}
36+
results.value = {
37+
items: data.results,
38+
heights: generateHeights(data.results),
39+
};
40+
});
41+
});
42+
return () => {
43+
unsub();
44+
};
45+
});
46+
47+
useSignalEffect(() => {
48+
term.subscribe((t) => {
49+
containerRef.current?.scrollTo(0, 0);
50+
});
1851
});
52+
1953
return (
2054
<div class={styles.layout} data-theme={isDarkMode ? 'dark' : 'light'}>
2155
<header class={styles.header}>
22-
<Header setResults={(next) => (results.value = next)} />
56+
<Header />
2357
</header>
2458
<aside class={styles.aside}>
25-
<h1 class={styles.pageTitle}>History</h1>
59+
<Sidebar ranges={initial.ranges.ranges} />
2660
</aside>
27-
<main class={styles.main}>
28-
<Results results={results} />
61+
<main class={styles.main} ref={containerRef} data-main-scroller data-term={term}>
62+
<Results results={results} term={term} containerRef={containerRef} />
2963
</main>
3064
</div>
3165
);
Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@import url("../../../../shared/styles/variables.css");
22
@import url("../../styles/base.css");
3-
@import url("../../styles/theme.css");
3+
@import url("../../styles/history-theme.css");
44

55
body {
66
font-size: var(--body-font-size);
@@ -11,19 +11,18 @@ body {
1111
.layout {
1212
display: grid;
1313
grid-template-columns: 250px 1fr;
14-
grid-template-rows: 64px auto;
14+
grid-template-rows: max-content auto;
1515
grid-template-areas:
1616
'aside header'
1717
'aside main';
1818
overflow: hidden;
1919
height: 100vh;
20+
background-color: var(--history-background-color);
2021
}
2122
.header {
2223
grid-area: header;
2324
padding-left: 48px;
2425
padding-right: 76px;
25-
padding-top: 16px;
26-
padding-bottom: 16px;
2726
}
2827
.search {
2928
justify-self: flex-end;
@@ -33,15 +32,10 @@ body {
3332
padding: 10px 16px;
3433
border-right: 1px solid var(--history-surface-border-color);
3534
}
36-
.pageTitle {
37-
font-size: var(--title-font-size);
38-
font-weight: var(--title-font-weight);
39-
line-height: var(--title-line-height);
40-
padding: 10px 6px 10px 10px;
41-
}
4235
.main {
4336
grid-area: main;
4437
overflow: auto;
4538
padding-left: 48px;
4639
padding-right: 76px;
40+
padding-top: 24px;
4741
}

special-pages/pages/history/app/components/Header.js

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,24 @@
11
import styles from './Header.module.css';
2-
import { Fire } from '../icons/Fire.js';
32
import { h } from 'preact';
4-
import { useMessaging, useTypedTranslation } from '../types.js';
5-
import { Cross } from '../icons/Cross.js';
6-
import { useEffect } from 'preact/hooks';
3+
import { useComputed } from '@preact/signals';
4+
import { SearchForm, useSearchContext } from './SearchForm.js';
5+
import { Trash } from '../icons/Trash.js';
76

8-
export function Header({ setResults }) {
9-
const { t } = useTypedTranslation();
10-
const historyPage = useMessaging();
11-
useEffect(() => {
12-
historyPage
13-
.query({ term: '', limit: 150, offset: 0 })
14-
// eslint-disable-next-line promise/prefer-await-to-then
15-
.then(setResults)
16-
// eslint-disable-next-line promise/prefer-await-to-then
17-
.catch((e) => {
18-
console.log('did catch...', e);
19-
});
20-
}, []);
7+
/**
8+
*/
9+
export function Header() {
10+
const search = useSearchContext();
11+
const term = useComputed(() => search.value.term);
2112
return (
2213
<div class={styles.root}>
2314
<div class={styles.controls}>
24-
<button class={styles.largeButton}>
25-
<Fire />
26-
<span>Clear History and Data...</span>
27-
</button>
28-
<button class={styles.largeButton}>
29-
<Cross />
30-
<span>Remove History...</span>
15+
<button class={styles.largeButton} data-delete-all>
16+
<span>Delete All</span>
17+
<Trash />
3118
</button>
3219
</div>
3320
<div class={styles.search}>
34-
<form
35-
action=""
36-
onSubmit={(e) => {
37-
e.preventDefault();
38-
const data = new FormData(/** @type {HTMLFormElement} */ (e.target));
39-
historyPage
40-
.query({ term: data.get('term')?.toString() || '', limit: 150, offset: 0 })
41-
// eslint-disable-next-line promise/prefer-await-to-then
42-
.then(setResults)
43-
// eslint-disable-next-line promise/prefer-await-to-then
44-
.catch((e) => {
45-
console.log('did catch...', e);
46-
});
47-
}}
48-
>
49-
<input type="search" placeholder={t('search')} class={styles.searchInput} name="term" />
50-
</form>
21+
<SearchForm term={term} />
5122
</div>
5223
</div>
5324
);

special-pages/pages/history/app/components/Header.module.css

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,48 @@
22
display: flex;
33
align-items: center;
44
gap: 8px;
5+
color: var(--history-text-normal);
6+
padding: 16px 0;
7+
border-bottom: 1px solid var(--history-surface-border-color);
58
}
9+
610
.controls {
711
display: flex;
812
gap: 8px;
913
}
14+
1015
.largeButton {
1116
background: transparent;
1217
display: flex;
1318
align-items: center;
14-
gap: 4px;
15-
height: 32px;
19+
gap: 6px;
20+
height: 28px;
1621
border: none;
22+
border-radius: 4px;
23+
color: var(--history-text-normal);
24+
padding-left: 8px;
25+
padding-right: 8px;
1726

1827
svg {
19-
flex-shrink: 0
28+
flex-shrink: 0;
29+
fill-opacity: 0.84;
2030
}
2131

2232
&:hover {
23-
background-color: var(--history-surface-color)
33+
background-color: var(--color-black-at-6);
34+
[data-theme="dark"] & {
35+
background-color: var(--color-white-at-6)
36+
}
37+
}
38+
39+
&:active {
40+
background-color: var(--color-black-at-12);
41+
[data-theme="dark"] & {
42+
background-color: var(--color-white-at-12)
43+
}
2444
}
2545
}
46+
2647
.search {
2748
margin-left: auto;
2849
}

0 commit comments

Comments
 (0)