@peektravel/app-utilities 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -27
- package/dist/index.cjs +138 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -1
- package/dist/index.d.ts +81 -1
- package/dist/index.js +138 -1
- package/dist/index.js.map +1 -1
- package/dist/ui/index.cjs +3932 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +1481 -0
- package/dist/ui/index.d.ts +1481 -0
- package/dist/ui/index.js +3853 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/odyssey.css +782 -0
- package/dist/ui/tokens.css +104 -0
- package/docs/ui.md +1001 -0
- package/llms.txt +49 -5
- package/package.json +22 -4
package/README.md
CHANGED
|
@@ -7,26 +7,15 @@ high-level `PeekAccessService` and the plain data shapes it returns.
|
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
|
-
This package is published to
|
|
11
|
-
|
|
12
|
-
consuming project by adding an `.npmrc` next to its `package.json`:
|
|
13
|
-
|
|
14
|
-
```ini
|
|
15
|
-
@peek-travel:registry=https://npm.pkg.github.com
|
|
16
|
-
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Then install (and later update) it like any other dependency:
|
|
10
|
+
This package is published to the **public npm registry**. Install (and later
|
|
11
|
+
update) it like any other dependency — no registry config or auth token needed:
|
|
20
12
|
|
|
21
13
|
```bash
|
|
22
14
|
npm install @peektravel/app-utilities
|
|
23
15
|
npm update @peektravel/app-utilities
|
|
24
16
|
```
|
|
25
17
|
|
|
26
|
-
|
|
27
|
-
a personal access token in your environment; in cloud builds (Firebase
|
|
28
|
-
Functions / Google Cloud Build, CI) set it as a build secret/env var. See
|
|
29
|
-
[Releasing](#releasing-github-packages) for how new versions are published.
|
|
18
|
+
See [Releasing](#releasing) for how new versions are published.
|
|
30
19
|
|
|
31
20
|
## Usage
|
|
32
21
|
|
|
@@ -207,13 +196,90 @@ both `import` and `require` consumers (including the Node 22 / CommonJS Firebase
|
|
|
207
196
|
Functions runtime) resolve correctly. Its only runtime dependency is
|
|
208
197
|
`jsonwebtoken`.
|
|
209
198
|
|
|
210
|
-
##
|
|
199
|
+
## UI components (`/ui`)
|
|
200
|
+
|
|
201
|
+
The package also ships framework-agnostic **Web Components** ported from the Peek
|
|
202
|
+
Odyssey design system, under a separate browser-only subpath so the server
|
|
203
|
+
library stays DOM-free. They work in any HTML page — no framework required.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// Registers every <ody-*> custom element as a side effect.
|
|
207
|
+
import '@peektravel/app-utilities/ui';
|
|
208
|
+
import '@peektravel/app-utilities/ui/tokens.css';
|
|
209
|
+
import '@peektravel/app-utilities/ui/odyssey.css';
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```html
|
|
213
|
+
<ody-button variant="primary" left-icon="plus">New booking</ody-button>
|
|
214
|
+
<ody-tag color="success" icon="check">Confirmed</ody-tag>
|
|
215
|
+
<ody-alert variant="warning" heading="Heads up">This can't be undone.</ody-alert>
|
|
216
|
+
<ody-input label="Guest name" placeholder="Jane Doe"></ody-input>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Coverage spans display (button, tag, alert, card, status-dot, message, icon,
|
|
220
|
+
loading-spinner/bar, divider), layout (empty-state, breadcrumb, stat-summary,
|
|
221
|
+
inline-list, list-item, product-indicator, toggle-button, section, two-column,
|
|
222
|
+
collapsible-section), form inputs (input, inline/search/money/percentage input,
|
|
223
|
+
checkbox, radio-button-group, checkbox-group), interactive (accordion,
|
|
224
|
+
collapsible, tabs, copy-button, check-in-status, option, split-button,
|
|
225
|
+
table-header), overlays (modal, popover, tooltip, panel, toast), and data &
|
|
226
|
+
selection (dropdown-single, dropdown-multi, datepicker, table — all vanilla and
|
|
227
|
+
dependency-free, following WAI-ARIA combobox/listbox/grid patterns).
|
|
228
|
+
|
|
229
|
+
Interactive components reflect state and emit `CustomEvent`s; grouped components
|
|
230
|
+
(tabs, radio/checkbox groups, toggle group) take a JSON `options`/`tabs`
|
|
231
|
+
attribute. Exported classes/types and helpers (`iconSvg`, `registerIcon`,
|
|
232
|
+
`portal`, `position`, `toast`) are available from `@peektravel/app-utilities/ui`
|
|
233
|
+
for subclassing or typing.
|
|
234
|
+
|
|
235
|
+
**Try the gallery:** `npm run sample` builds the package and serves
|
|
236
|
+
`examples/ui-gallery.html`, which shows every component with its variants.
|
|
237
|
+
|
|
238
|
+
### Localization
|
|
239
|
+
|
|
240
|
+
Content you pass in (labels, headings, options, cell data) is already yours to
|
|
241
|
+
localize. The components' **own** built-in strings (aria-labels like "Close" /
|
|
242
|
+
"Clear", the date picker's "Select date" and month-nav labels, the dropdown
|
|
243
|
+
"Search" / "No options", the check-in-status labels) are translatable two ways:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { registerTranslation } from '@peektravel/app-utilities/ui';
|
|
247
|
+
|
|
248
|
+
registerTranslation('es', {
|
|
249
|
+
close: 'Cerrar', clear: 'Borrar', search: 'Buscar',
|
|
250
|
+
checkInReturned: 'Devuelto', /* … */
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Each component resolves its language from the nearest `lang` attribute
|
|
255
|
+
(`<html lang="es">` localizes everything; a subtree `lang` overrides it), and
|
|
256
|
+
re-renders automatically when the language or a registered bundle changes.
|
|
257
|
+
English is the built-in default. For a one-off, a per-instance attribute wins:
|
|
258
|
+
`<ody-panel close-label="Cerrar">`, `<ody-datepicker next-month-label="…">`,
|
|
259
|
+
`<ody-check-in-status label="…">`.
|
|
260
|
+
|
|
261
|
+
The **date picker** displays dates with `Intl.DateTimeFormat` for the resolved
|
|
262
|
+
`lang` — a readable, localized label (e.g. "Jun 15, 2026" / "15 jun 2026")
|
|
263
|
+
rather than the raw ISO string. Its `value` attribute and `change` payload stay
|
|
264
|
+
machine-readable ISO `yyyy-mm-dd` (range as `start/end`). Tune the displayed
|
|
265
|
+
form with `display-format` (`short` | `medium` | `long` | `full`) or take full
|
|
266
|
+
control with the `formatDate` property. Weekday/month names and day labels in
|
|
267
|
+
the calendar are likewise `Intl`-localized, so they aren't in the term catalog.
|
|
268
|
+
|
|
269
|
+
> A few Odyssey components remain unported: `nested-multi-select`,
|
|
270
|
+
> `location-autocomplete` (Google Maps API), `filter-menu` / `filter-menu-single`,
|
|
271
|
+
> `accordion-checkbox`, and `datepicker-with-presets`. The dropdowns, the single
|
|
272
|
+
> date picker, and the data table were rebuilt here as lightweight,
|
|
273
|
+
> dependency-free vanilla components rather than ported from their
|
|
274
|
+
> third-party-coupled Ember originals.
|
|
275
|
+
|
|
276
|
+
## Releasing
|
|
211
277
|
|
|
212
278
|
Releases are automated. Pushing a `v*.*.*` git tag triggers
|
|
213
279
|
`.github/workflows/publish.yml`, which typechecks, lints, runs the test suite
|
|
214
|
-
(95% coverage gate), then publishes to
|
|
215
|
-
`prepublishOnly` first, so the build plus `publint` + `attw` checks gate
|
|
216
|
-
release.
|
|
280
|
+
(95% coverage gate), then publishes to the public npm registry. `npm publish`
|
|
281
|
+
runs `prepublishOnly` first, so the build plus `publint` + `attw` checks gate
|
|
282
|
+
every release.
|
|
217
283
|
|
|
218
284
|
To cut a release:
|
|
219
285
|
|
|
@@ -223,13 +289,14 @@ git push --follow-tags # pushes the commit + tag; the workflow publishes
|
|
|
223
289
|
```
|
|
224
290
|
|
|
225
291
|
The workflow asserts the tag matches the `package.json` version, so the two
|
|
226
|
-
never drift.
|
|
227
|
-
|
|
228
|
-
|
|
292
|
+
never drift. Publish auth uses an `NPM_TOKEN` repository secret (an npm
|
|
293
|
+
automation token with publish rights to the `@peektravel` scope), exposed to
|
|
294
|
+
`npm publish` as `NODE_AUTH_TOKEN`. Consumers then pick the new version up with
|
|
295
|
+
a normal `npm update` (see [Install](#install)).
|
|
229
296
|
|
|
230
|
-
> One-time setup:
|
|
231
|
-
>
|
|
232
|
-
>
|
|
297
|
+
> One-time setup: add an `NPM_TOKEN` secret to the repository (Settings →
|
|
298
|
+
> Secrets and variables → Actions). Generate it on npmjs.com as an **Automation**
|
|
299
|
+
> token so it bypasses 2FA in CI.
|
|
233
300
|
|
|
234
301
|
## Development
|
|
235
302
|
|
|
@@ -249,13 +316,15 @@ npm run lint # eslint (flat config)
|
|
|
249
316
|
and [`@arethetypeswrong/cli`](https://github.com/arethetypeswrong/arethetypeswrong.github.io)
|
|
250
317
|
to verify the `exports` map and type resolution are correct for both module
|
|
251
318
|
systems. The publish workflow runs these automatically — see
|
|
252
|
-
[Releasing](#releasing
|
|
319
|
+
[Releasing](#releasing).
|
|
253
320
|
|
|
254
321
|
## Project layout
|
|
255
322
|
|
|
256
323
|
```
|
|
257
|
-
src/ source (public API barrel: src/index.ts)
|
|
258
|
-
|
|
324
|
+
src/ server library source (public API barrel: src/index.ts)
|
|
325
|
+
src/ui/ Web Components + Odyssey CSS (barrel: src/ui/index.ts)
|
|
326
|
+
test/ vitest unit tests (test/ui/* run under happy-dom)
|
|
327
|
+
examples/ui-gallery.html component gallery (npm run sample)
|
|
259
328
|
dist/ build output (generated, git-ignored)
|
|
260
329
|
docs/internal/ maintainer docs (ARCHITECTURE.md — not shipped)
|
|
261
330
|
llms.txt AI-agent quickstart (shipped in the package)
|
package/dist/index.cjs
CHANGED
|
@@ -2151,6 +2151,132 @@ var ResourcePoolService = class {
|
|
|
2151
2151
|
}
|
|
2152
2152
|
};
|
|
2153
2153
|
|
|
2154
|
+
// src/internal/reviews/review-converter.ts
|
|
2155
|
+
var ISO_DATE_LENGTH = 10;
|
|
2156
|
+
function toDateOnly(isoDateTime) {
|
|
2157
|
+
return isoDateTime.slice(0, ISO_DATE_LENGTH);
|
|
2158
|
+
}
|
|
2159
|
+
function fromReviewNode(node) {
|
|
2160
|
+
const guides = (node.guides ?? []).map((guide) => ({
|
|
2161
|
+
id: guide.id,
|
|
2162
|
+
name: guide.name
|
|
2163
|
+
}));
|
|
2164
|
+
return {
|
|
2165
|
+
id: node.id,
|
|
2166
|
+
productId: node.activity?.id ?? "",
|
|
2167
|
+
productName: node.activity?.name ?? "",
|
|
2168
|
+
guides,
|
|
2169
|
+
customerName: node.name ?? null,
|
|
2170
|
+
customerEmail: node.email ?? null,
|
|
2171
|
+
activityDate: toDateOnly(node.purchasedFor),
|
|
2172
|
+
reviewDate: toDateOnly(node.reviewedAt),
|
|
2173
|
+
rating: node.rating,
|
|
2174
|
+
comment: node.comment ?? null
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// src/internal/reviews/review-cursor.ts
|
|
2179
|
+
function encodeCursor(offset, pageSize) {
|
|
2180
|
+
const start = Math.max(0, offset - pageSize + 1);
|
|
2181
|
+
return Buffer.from(`range:${start}..${offset},${offset}`).toString("base64");
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// src/internal/reviews/review-queries.ts
|
|
2185
|
+
var REVIEWS_QUERY = `
|
|
2186
|
+
query Reviews($first: Int, $filter: ReviewFilter, $after: String) {
|
|
2187
|
+
reviews(first: $first, filter: $filter, after: $after) {
|
|
2188
|
+
edges {
|
|
2189
|
+
node {
|
|
2190
|
+
activity {
|
|
2191
|
+
id
|
|
2192
|
+
name
|
|
2193
|
+
}
|
|
2194
|
+
guides {
|
|
2195
|
+
id
|
|
2196
|
+
name
|
|
2197
|
+
}
|
|
2198
|
+
id
|
|
2199
|
+
name
|
|
2200
|
+
email
|
|
2201
|
+
rating
|
|
2202
|
+
comment
|
|
2203
|
+
reviewedAt
|
|
2204
|
+
purchasedFor
|
|
2205
|
+
}
|
|
2206
|
+
cursor
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
`;
|
|
2211
|
+
function buildReviewsVariables(params) {
|
|
2212
|
+
return {
|
|
2213
|
+
first: params.first,
|
|
2214
|
+
filter: { activityIds: [params.activityId] },
|
|
2215
|
+
after: params.after
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// src/internal/reviews/review-service.ts
|
|
2220
|
+
var DEFAULT_REVIEW_COUNT = 50;
|
|
2221
|
+
var MIN_REVIEW_COUNT = 1;
|
|
2222
|
+
var MAX_REVIEW_COUNT = 50;
|
|
2223
|
+
var ERROR_PRODUCT_ID_REQUIRED = "productId is required";
|
|
2224
|
+
var ERROR_INVALID_REVIEW_COUNT = `reviewCount must be an integer between ${MIN_REVIEW_COUNT} and ${MAX_REVIEW_COUNT}`;
|
|
2225
|
+
var ERROR_INVALID_REVIEW_OFFSET = "reviewOffset must be a non-negative integer";
|
|
2226
|
+
var ReviewService = class {
|
|
2227
|
+
constructor(client) {
|
|
2228
|
+
this.client = client;
|
|
2229
|
+
}
|
|
2230
|
+
client;
|
|
2231
|
+
/**
|
|
2232
|
+
* Returns up to `reviewCount` reviews for an activity in **descending order
|
|
2233
|
+
* by review date (newest first)**, skipping the `reviewOffset` newest reviews
|
|
2234
|
+
* before collecting.
|
|
2235
|
+
*
|
|
2236
|
+
* The gateway cursor resumes *after* a given absolute offset, so to skip the
|
|
2237
|
+
* first `reviewOffset` reviews the request is anchored on the review just
|
|
2238
|
+
* before the window. A `reviewOffset` of 0 needs no cursor and is sent
|
|
2239
|
+
* without one.
|
|
2240
|
+
*
|
|
2241
|
+
* @param productId - The activity (product) id to fetch reviews for.
|
|
2242
|
+
* @param reviewCount - How many reviews to return (1–50). Default: 50.
|
|
2243
|
+
* @param reviewOffset - How many of the newest reviews to skip first
|
|
2244
|
+
* (0-based). Default: 0 (start at the newest review).
|
|
2245
|
+
*
|
|
2246
|
+
* @throws {Error} when `productId` is empty, `reviewCount` is not an integer
|
|
2247
|
+
* in 1–50, or `reviewOffset` is not a non-negative integer.
|
|
2248
|
+
*
|
|
2249
|
+
* @example
|
|
2250
|
+
* ```ts
|
|
2251
|
+
* const reviews = await peek
|
|
2252
|
+
* .getReviewService()
|
|
2253
|
+
* .getReviews("87cdf37f-1872-42cb-b0bd-518312624fc1", 25, 50);
|
|
2254
|
+
* ```
|
|
2255
|
+
*/
|
|
2256
|
+
async getReviews(productId, reviewCount = DEFAULT_REVIEW_COUNT, reviewOffset = 0) {
|
|
2257
|
+
this.validate(productId, reviewCount, reviewOffset);
|
|
2258
|
+
const after = reviewOffset > 0 ? encodeCursor(reviewOffset - 1, reviewCount) : null;
|
|
2259
|
+
const body = await this.client.request(
|
|
2260
|
+
SALES_ENDPOINT,
|
|
2261
|
+
REVIEWS_QUERY,
|
|
2262
|
+
buildReviewsVariables({ activityId: productId, first: reviewCount, after })
|
|
2263
|
+
);
|
|
2264
|
+
const edges = body.data?.reviews?.edges ?? [];
|
|
2265
|
+
return edges.map((edge) => fromReviewNode(edge.node));
|
|
2266
|
+
}
|
|
2267
|
+
validate(productId, reviewCount, reviewOffset) {
|
|
2268
|
+
if (!productId) {
|
|
2269
|
+
throw new Error(ERROR_PRODUCT_ID_REQUIRED);
|
|
2270
|
+
}
|
|
2271
|
+
if (!Number.isInteger(reviewCount) || reviewCount < MIN_REVIEW_COUNT || reviewCount > MAX_REVIEW_COUNT) {
|
|
2272
|
+
throw new Error(ERROR_INVALID_REVIEW_COUNT);
|
|
2273
|
+
}
|
|
2274
|
+
if (!Number.isInteger(reviewOffset) || reviewOffset < 0) {
|
|
2275
|
+
throw new Error(ERROR_INVALID_REVIEW_OFFSET);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2279
|
+
|
|
2154
2280
|
// src/internal/timeslots/timeslot-converter.ts
|
|
2155
2281
|
function fromTimeslotNodes(nodes, productId) {
|
|
2156
2282
|
if (!Array.isArray(nodes) || nodes.length === 0) {
|
|
@@ -2798,6 +2924,7 @@ var PeekAccessService = class {
|
|
|
2798
2924
|
availabilityService;
|
|
2799
2925
|
membershipService;
|
|
2800
2926
|
bookingService;
|
|
2927
|
+
reviewService;
|
|
2801
2928
|
constructor(config) {
|
|
2802
2929
|
requireNonEmpty(config.installId, "installId");
|
|
2803
2930
|
requireNonEmpty(config.jwtSecret, "jwtSecret");
|
|
@@ -2934,6 +3061,16 @@ var PeekAccessService = class {
|
|
|
2934
3061
|
}
|
|
2935
3062
|
return this.bookingService;
|
|
2936
3063
|
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Returns the {@link ReviewService} for this install, bound to the shared
|
|
3066
|
+
* authenticated transport. The instance is created lazily and reused.
|
|
3067
|
+
*/
|
|
3068
|
+
getReviewService() {
|
|
3069
|
+
if (!this.reviewService) {
|
|
3070
|
+
this.reviewService = new ReviewService(this.client);
|
|
3071
|
+
}
|
|
3072
|
+
return this.reviewService;
|
|
3073
|
+
}
|
|
2937
3074
|
};
|
|
2938
3075
|
function requireNonEmpty(value, name) {
|
|
2939
3076
|
if (!value) {
|
|
@@ -2955,6 +3092,7 @@ exports.PromoCodeService = PromoCodeService;
|
|
|
2955
3092
|
exports.RateLimitError = RateLimitError;
|
|
2956
3093
|
exports.ResellerService = ResellerService;
|
|
2957
3094
|
exports.ResourcePoolService = ResourcePoolService;
|
|
3095
|
+
exports.ReviewService = ReviewService;
|
|
2958
3096
|
exports.TimeslotService = TimeslotService;
|
|
2959
3097
|
exports.noopLogger = noopLogger;
|
|
2960
3098
|
//# sourceMappingURL=index.cjs.map
|