@tribe-nest/forge 3.31.0 → 3.35.0
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/package.json +9 -3
- package/src/client/_tests/tenantHeaders.spec.ts +77 -0
- package/src/client/createForgeClient.ts +37 -0
- package/src/contexts/CartContext.tsx +17 -1
- package/src/contexts/_tests/CartContext.spec.tsx +36 -0
- package/src/data/queries/useFilmPlaybackSession.ts +224 -0
- package/src/data/queries/useFilms.ts +561 -0
- package/src/data/queries/useMyBookings.ts +9 -0
- package/src/i18n/_tests/translationKeys.spec.ts +15 -0
- package/src/i18n/de.json +140 -2
- package/src/i18n/en.json +140 -2
- package/src/index.ts +5 -0
- package/src/provider/ForgeAppProvider.tsx +10 -0
- package/src/provider/ForgeProvider.tsx +18 -3
- package/src/server/index.ts +76 -17
- package/src/ui/headless/film/FilmWatermark.tsx +180 -0
- package/src/ui/headless/film/_tests/filmRules.spec.ts +531 -0
- package/src/ui/headless/film/_tests/useStageFullscreen.spec.ts +167 -0
- package/src/ui/headless/film/index.ts +36 -0
- package/src/ui/headless/film/useFilmCatalog.ts +66 -0
- package/src/ui/headless/film/useFilmPlayback.ts +497 -0
- package/src/ui/headless/film/useFilmRentalFlow.ts +277 -0
- package/src/ui/headless/film/useStageFullscreen.ts +156 -0
- package/src/ui/headless/index.ts +6 -9
- package/src/ui/index.ts +43 -16
- package/src/ui/media/BookingCallScreen.tsx +33 -0
- package/src/ui/media/CallStage.tsx +212 -63
- package/src/ui/media/CallWindowNotice.tsx +94 -0
- package/src/ui/media/_tests/CallStage.spec.tsx +265 -19
- package/src/ui/media/_tests/CallWindowNotice.spec.tsx +83 -0
- package/src/ui/media/_tests/bookingSession.spec.tsx +48 -0
- package/src/ui/media/_tests/callState.spec.ts +63 -16
- package/src/ui/media/bookingSession.tsx +45 -57
- package/src/ui/media/bookingWindow.ts +81 -0
- package/src/ui/media/callState.ts +42 -23
- package/src/ui/media/index.ts +9 -0
- package/src/ui/styled/AccountDashboard.tsx +97 -5
- package/src/ui/styled/FilmCatalog.tsx +278 -0
- package/src/ui/styled/FilmDetail.tsx +661 -0
- package/src/ui/styled/FilmLibrary.tsx +254 -0
- package/src/ui/styled/FilmWatch.tsx +701 -0
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +194 -0
- package/src/ui/styled/forge-utilities.css +835 -0
- package/src/utils/structuredData.ts +88 -17
|
@@ -10,22 +10,15 @@ export function buildOrganizationSchema(webPage: WebPage, origin: string) {
|
|
|
10
10
|
"@type": "Organization",
|
|
11
11
|
name: webPage.profile.name,
|
|
12
12
|
url: origin,
|
|
13
|
-
...(webPage.themeSettings.header?.logo
|
|
14
|
-
|
|
15
|
-
: {}),
|
|
16
|
-
...(socialLinks.length > 0
|
|
17
|
-
? { sameAs: socialLinks.map((l: { href: string }) => l.href).filter(Boolean) }
|
|
18
|
-
: {}),
|
|
13
|
+
...(webPage.themeSettings.header?.logo ? { logo: webPage.themeSettings.header.logo } : {}),
|
|
14
|
+
...(socialLinks.length > 0 ? { sameAs: socialLinks.map((l: { href: string }) => l.href).filter(Boolean) } : {}),
|
|
19
15
|
};
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
/**
|
|
23
19
|
* Generates WebPage JSON-LD.
|
|
24
20
|
*/
|
|
25
|
-
export function buildWebPageSchema(
|
|
26
|
-
webPage: WebPage,
|
|
27
|
-
canonicalUrl: string,
|
|
28
|
-
) {
|
|
21
|
+
export function buildWebPageSchema(webPage: WebPage, canonicalUrl: string) {
|
|
29
22
|
return {
|
|
30
23
|
"@context": "https://schema.org",
|
|
31
24
|
"@type": "WebPage",
|
|
@@ -181,10 +174,7 @@ export function buildEntitySchema(input: EntitySchemaInput): Record<string, unkn
|
|
|
181
174
|
|
|
182
175
|
const { aggregate } = input;
|
|
183
176
|
const hasRatings =
|
|
184
|
-
!!aggregate &&
|
|
185
|
-
aggregate.reviewCount > 0 &&
|
|
186
|
-
Number.isFinite(aggregate.avgRating) &&
|
|
187
|
-
aggregate.avgRating > 0;
|
|
177
|
+
!!aggregate && aggregate.reviewCount > 0 && Number.isFinite(aggregate.avgRating) && aggregate.avgRating > 0;
|
|
188
178
|
|
|
189
179
|
const reviews = (input.reviews ?? [])
|
|
190
180
|
.filter((r) => r.rating >= 1 && r.rating <= 5)
|
|
@@ -439,12 +429,93 @@ export function buildServiceSchema(
|
|
|
439
429
|
aggregate: service.reviewAggregate ?? null,
|
|
440
430
|
reviews: options.reviews,
|
|
441
431
|
offer:
|
|
442
|
-
options.currency && price !== undefined
|
|
443
|
-
? { currency: options.currency, price, availability: "InStock" }
|
|
444
|
-
: null,
|
|
432
|
+
options.currency && price !== undefined ? { currency: options.currency, price, availability: "InStock" } : null,
|
|
445
433
|
});
|
|
446
434
|
}
|
|
447
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Structural view of `GET /public/films/:slug`. `PublicFilmDetail` satisfies it.
|
|
438
|
+
*
|
|
439
|
+
* Deliberately narrow, and deliberately NOT the whole payload: JSON-LD is
|
|
440
|
+
* emitted into an anonymous SSR document, so anything named here is something a
|
|
441
|
+
* crawler and a signed-out visitor both get to read. Nothing about a rental,
|
|
442
|
+
* a session or an episode's storage belongs in it.
|
|
443
|
+
*/
|
|
444
|
+
export interface FilmSchemaSource {
|
|
445
|
+
kind: "film" | "series";
|
|
446
|
+
title: string;
|
|
447
|
+
description?: string | null;
|
|
448
|
+
cover?: { url: string } | null;
|
|
449
|
+
currency: string;
|
|
450
|
+
rentalPriceCents?: number | null;
|
|
451
|
+
publishedAt?: string | null;
|
|
452
|
+
episodes?: { durationSec?: number | null }[] | null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** ISO 8601 duration (`PT1H42M`) from whole seconds, or undefined when unknown. */
|
|
456
|
+
const isoDuration = (seconds?: number | null): string | undefined => {
|
|
457
|
+
if (seconds == null || !Number.isFinite(seconds) || seconds <= 0) return undefined;
|
|
458
|
+
const whole = Math.floor(seconds);
|
|
459
|
+
const hours = Math.floor(whole / 3600);
|
|
460
|
+
const minutes = Math.round((whole % 3600) / 60);
|
|
461
|
+
if (!hours && !minutes) return undefined;
|
|
462
|
+
return `PT${hours ? `${hours}H` : ""}${minutes ? `${minutes}M` : ""}`;
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* schema.org/Movie or /TVSeries for a rentable title.
|
|
467
|
+
*
|
|
468
|
+
* Built by hand rather than through `buildEntitySchema`, whose `type` union is
|
|
469
|
+
* closed over the three reviewable sellables. Widening that union to carry two
|
|
470
|
+
* types that have no review surface would make every caller of it responsible
|
|
471
|
+
* for cases it can never see.
|
|
472
|
+
*
|
|
473
|
+
* The offer is the title's rental price, which is the only price there is: a
|
|
474
|
+
* rental always buys the whole title. There is no cheaper way in to quote, and
|
|
475
|
+
* a title with no price gets no `offers` node rather than a free one.
|
|
476
|
+
*/
|
|
477
|
+
export function buildFilmSchema(
|
|
478
|
+
film: FilmSchemaSource | null | undefined,
|
|
479
|
+
options: SellableSchemaOptions = {},
|
|
480
|
+
): Record<string, unknown> | null {
|
|
481
|
+
const name = film?.title?.trim();
|
|
482
|
+
if (!film || !name) return null;
|
|
483
|
+
|
|
484
|
+
const priceCents =
|
|
485
|
+
typeof film.rentalPriceCents === "number" && Number.isFinite(film.rentalPriceCents) ? film.rentalPriceCents : null;
|
|
486
|
+
const currency = options.currency ?? film.currency;
|
|
487
|
+
const description = toSchemaText(film.description);
|
|
488
|
+
// A film's runtime is its single episode's. A series has no one runtime, so it
|
|
489
|
+
// gets none rather than an invented average.
|
|
490
|
+
const duration = film.kind === "film" ? isoDuration(film.episodes?.[0]?.durationSec) : undefined;
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
"@context": "https://schema.org",
|
|
494
|
+
"@type": film.kind === "series" ? "TVSeries" : "Movie",
|
|
495
|
+
name,
|
|
496
|
+
...(description ? { description } : {}),
|
|
497
|
+
...(film.cover?.url ? { image: film.cover.url } : {}),
|
|
498
|
+
...(options.url ? { url: options.url } : {}),
|
|
499
|
+
...(options.brand ? { provider: { "@type": "Organization", name: options.brand } } : {}),
|
|
500
|
+
...(film.publishedAt ? { datePublished: film.publishedAt } : {}),
|
|
501
|
+
...(duration ? { duration } : {}),
|
|
502
|
+
...(film.kind === "series" && film.episodes?.length ? { numberOfEpisodes: film.episodes.length } : {}),
|
|
503
|
+
...(currency && priceCents != null
|
|
504
|
+
? {
|
|
505
|
+
offers: {
|
|
506
|
+
"@type": "Offer",
|
|
507
|
+
priceCurrency: currency,
|
|
508
|
+
price: money(priceCents / 100),
|
|
509
|
+
availability: "https://schema.org/InStock",
|
|
510
|
+
// A rental, not a sale. Saying so is what keeps a $2 line from
|
|
511
|
+
// reading as the price of owning the film.
|
|
512
|
+
category: "Rental",
|
|
513
|
+
},
|
|
514
|
+
}
|
|
515
|
+
: {}),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
448
519
|
/**
|
|
449
520
|
* The `scripts` entry a TanStack `head()` needs for a JSON-LD node. Returns an
|
|
450
521
|
* empty array for a null schema, so it can be spread unconditionally:
|