Skip to content

Commit ff05032

Browse files
committed
examples banners
1 parent b8edcd1 commit ff05032

5 files changed

Lines changed: 81 additions & 10 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script lang="ts">
2+
import type { HomePartnerId } from '$lib/partners/home-rotation.js';
3+
import PartnerBannerCursor from './PartnerBannerCursor.svelte';
4+
import PartnerBannerPreply from './PartnerBannerPreply.svelte';
5+
import PartnerBannerRailway from './PartnerBannerRailway.svelte';
6+
import PartnerBannerWise from './PartnerBannerWise.svelte';
7+
8+
let { partnerId }: { partnerId: HomePartnerId } = $props();
9+
10+
const bannerById = {
11+
preply: PartnerBannerPreply,
12+
railway: PartnerBannerRailway,
13+
cursor: PartnerBannerCursor,
14+
wise: PartnerBannerWise
15+
} as const;
16+
17+
const Banner = $derived(bannerById[partnerId]);
18+
</script>
19+
20+
<Banner />
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from 'vitest';
2+
import {
3+
getExamplePagePartnerId,
4+
getHomePartnerOrder,
5+
HOME_PARTNER_IDS
6+
} from './home-rotation.js';
7+
8+
describe('getExamplePagePartnerId', () => {
9+
it('returns a pool member and is stable for the same slug', () => {
10+
const a = getExamplePagePartnerId('turkish-interlinear-gloss-ipa');
11+
const b = getExamplePagePartnerId('turkish-interlinear-gloss-ipa');
12+
expect(HOME_PARTNER_IDS).toContain(a);
13+
expect(a).toBe(b);
14+
});
15+
16+
it('can differ across slugs', () => {
17+
const ids = new Set(HOME_PARTNER_IDS.map((_, i) => getExamplePagePartnerId(`slug-${i}`)));
18+
expect(ids.size).toBeGreaterThan(1);
19+
});
20+
});
21+
22+
describe('getHomePartnerOrder', () => {
23+
it('returns two distinct partners', () => {
24+
const [a, b] = getHomePartnerOrder(1_700_000_000_000);
25+
expect(a).not.toBe(b);
26+
});
27+
});

bitext/src/lib/partners/home-rotation.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
/**
2-
* Home page shows two partner banners in fixed regions (intro column + settings sidebar).
3-
* Each hour (UTC, by Unix time) we pick two *distinct* partners from `HOME_PARTNER_IDS`;
4-
* all ids participate in rotation as the set grows.
2+
* Partner banner rotation.
53
*
6-
* Order is computed once per request on the server so SSR and hydration match.
4+
* Home: two distinct partners per UTC hour (`getHomePartnerOrder`).
5+
* Example gallery pages: one partner per slug, stable across prerender/build (`getExamplePagePartnerId`).
76
*/
87
export const HOME_PARTNER_IDS = ['preply', 'railway', 'cursor', 'wise'] as const;
98
export type HomePartnerId = (typeof HOME_PARTNER_IDS)[number];
@@ -17,6 +16,16 @@ function mulberry32(seed: number) {
1716
};
1817
}
1918

19+
/** FNV-1a 32-bit — stable string → integer for seeded picks. */
20+
function hashSeedString(input: string): number {
21+
let h = 0x811c9dc5;
22+
for (let i = 0; i < input.length; i++) {
23+
h ^= input.charCodeAt(i);
24+
h = Math.imul(h, 0x01000193);
25+
}
26+
return h | 0;
27+
}
28+
2029
/** Uniform random shuffle (Fisher–Yates). Mutates `items`. */
2130
function shuffleInPlace<T>(items: T[], rng: () => number): void {
2231
for (let i = items.length - 1; i > 0; i--) {
@@ -25,6 +34,17 @@ function shuffleInPlace<T>(items: T[], rng: () => number): void {
2534
}
2635
}
2736

37+
/**
38+
* One partner for a prerendered example page. Seed with gallery `slug` so each page keeps
39+
* the same banner across visits and SSR/hydration match; partners are spread across slugs.
40+
*/
41+
export function getExamplePagePartnerId(slug: string): HomePartnerId {
42+
const seed = Math.imul(hashSeedString(`example:${slug}`), 0x9e3779b1) | 0;
43+
const rng = mulberry32(seed);
44+
const index = Math.floor(rng() * HOME_PARTNER_IDS.length);
45+
return HOME_PARTNER_IDS[index]!;
46+
}
47+
2848
/**
2949
* `[introColumn, settingsSidebar]` — two different partners; every ordered pair among
3050
* `HOME_PARTNER_IDS` is equally likely for a given draw.

bitext/src/routes/examples/[slug]/+page.svelte

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { resolve } from '$app/paths';
44
import { ALIGNER_DISPLAY_NAME } from '$lib/brand.js';
55
import SiteFooter from '$lib/components/layout/SiteFooter.svelte';
6+
import PartnerBannerById from '$lib/components/partners/PartnerBannerById.svelte';
67
import type { PageProps } from './$types';
78
89
let { data }: PageProps = $props();
@@ -91,15 +92,16 @@
9192
</figcaption>
9293
</figure>
9394

94-
<p class="mt-8">
95+
<div class="mt-8 flex flex-wrap items-center gap-x-4 gap-y-3">
96+
<a href={resolve('/examples')} class="{linkClass} text-sm">← All examples</a>
9597
<a href="{resolve('/')}?example={exampleSlug}" class={ctaClass}>
9698
Open in Editor
9799
</a>
98-
</p>
100+
</div>
99101

100-
<p class="mt-6 text-sm">
101-
<a href={resolve('/examples')} class={linkClass}>← All examples</a>
102-
</p>
102+
<div class="mt-10">
103+
<PartnerBannerById partnerId={data.partnerId} />
104+
</div>
103105

104106
<SiteFooter />
105107
</main>

bitext/src/routes/examples/[slug]/+page.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const prerender = true;
22

33
import { error } from '@sveltejs/kit';
44
import { findGalleryBySlug, galleryPreviewImageFor, GALLERY_EXAMPLES } from '$lib/examples/catalog.js';
5+
import { getExamplePagePartnerId } from '$lib/partners/home-rotation.js';
56
import type { PageLoad } from './$types';
67

78
export function entries() {
@@ -14,6 +15,7 @@ export const load: PageLoad = ({ params }) => {
1415
return {
1516
entry,
1617
previewImageUrl: galleryPreviewImageFor(entry),
17-
exampleSlug: entry.slug
18+
exampleSlug: entry.slug,
19+
partnerId: getExamplePagePartnerId(entry.slug)
1820
};
1921
};

0 commit comments

Comments
 (0)