openvibers/openvibe.games Commits
Share: one Share sheet on worlds, results and scores (#289)
Changes
@@@ -6,6 +6,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).## [Unreleased]++### Added++++- Share: a creator world's page, a round's result page, the leaderboard and the in-game round result now open the network's one Share sheet (`data-ov-share-sheet`, openvibe-shared 4.114.0) with the page's public address. The result page now starts the shared navbar itself, which is what loads the sheet on the first press; `navigator.share` is gone.++### Fixed- **A shared /worlds link had no preview.** The creator worlds page carried no Open Graph or Twitter tags, so a@@@ -13,6 +13,7 @@/><!-- The OpenVibe Frame: the user's theme before first paint, then navbar + footer. --><!-- Deferred: a slow or unreachable openvibe.network never holds the first paint (the inline colours above cover it). -->++ <link rel="stylesheet" href="/shared/kit.css" /><script src="/shared/theme-loader.js" defer></script><script src="/shared/navbar.js" defer></script><script src="/shared/footer.js" defer></script>@@@ -250,6 +250,16 @@letter-spacing: 0.03em;text-decoration: none;}++ .cta-row {++ display: flex;++ flex-wrap: wrap;++ align-items: center;++ gap: 10px;++ margin-top: 22px;++ }++ .cta-row .play {++ margin-top: 0;++ }.note {margin-top: 14px;font-size: 14px;@@@ -259,6 +269,7 @@<script type="module" src="/src/pages/leaderboard.ts"></script><!-- The OpenVibe Frame (navbar + footer), as on every other page of the site: this page had only its own row oflinks (phone sweep, 2026-11-01). -->++ <link rel="stylesheet" href="/shared/kit.css" /><script src="/shared/navbar.js" defer></script><script src="/shared/footer.js" defer></script><script>@@@ -311,7 +322,10 @@><button id="next" type="button">Next</button></div></section>-- <a class="play" id="play-link" href="/play?game=salvage-rush">▶ Play a round</a>++ <div class="cta-row">++ <button class="ovk-btn ovk-btn--quiet" type="button" id="share" data-ov-share-sheet data-title="Salvage Rush leaderboard" data-url="https://openvibe.games/leaderboard.html?game=salvage-rush"><svg class="ovk-i" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20V9M7 14l5-5 5 5M5 4h14"/></svg><span>Share</span></button>++ <a class="play" id="play-link" href="/play?game=salvage-rush">▶ Play a round</a>++ </div><p class="note">"This week" starts on Monday, 00:00 UTC. These boards are kept by<a href="https://play.openvibe.network/">OpenVibe.Play</a>, with the achievements you have@@@ -31,6 +31,7 @@ const lede = document.getElementById('lede') as HTMLElementconst scoreHeader = document.getElementById('score-header') as HTMLElementconst playLink = document.getElementById('play-link') as HTMLAnchorElementconst youLine = document.getElementById('you') as HTMLElement++const shareButton = document.getElementById('share') as HTMLButtonElementconst COPY: Record<Game,@@@ -86,9 +87,20 @@ function cell(text: string, className?: string): HTMLTableCellElement {return td}++/** The one Share sheet sends the board on screen: its game and period, in its public address. */++function applyShare(): void {++ const copy = COPY[game]++ const query = new URLSearchParams({ game })++ if (period === 'week') query.set('period', 'week')++ shareButton.dataset.title = `${copy.title} leaderboard`++ shareButton.dataset.text = copy.lede++ shareButton.dataset.url = `https://openvibe.games/leaderboard.html?${query.toString()}`++}++function applyGame(): void {for (const tab of gameTabs) tab.setAttribute('aria-pressed', String(tab.dataset.game === game))const copy = COPY[game]++ applyShare()title.textContent = copy.titlelede.textContent = copy.ledescoreHeader.textContent = copy.score@@@ -123,6 +135,7 @@ function markMine(mine: Mine | null): void {}async function load(): Promise<void> {++ applyShare()for (const tab of periodTabs)tab.setAttribute('aria-pressed', String(tab.dataset.period === period))const copy = COPY[game]@@@ -67,6 +67,9 @@.rush-result-actions > * {flex: 1 1 8rem;min-height: 2.75rem;++}++.rush-result-actions > :not(.ovk-btn) {++ min-height: 2.75rem;display: inline-flex;align-items: center;justify-content: center;@@@ -81,11 +84,6 @@text-decoration: none;cursor: pointer;}--.rush-result-share {-- background: var(--accent-strong, var(--accent));-- border-color: transparent;-- color: var(--on-accent-strong, var(--on-accent, #fff));--}.rush-result-board {background: transparent;}@@@ -39,7 +39,15 @@ describe('the result card of a round', () => {it('carries the buttons a player needs', () => {const card = buildResultCard(result, 'https://openvibe.games')-- expect(card.querySelector('.rush-result-share')?.textContent).toBe('Share')++ const share = card.querySelector('.rush-result-share') as HTMLButtonElement++ expect(share.className).toContain('ovk-btn ovk-btn--quiet')++ expect(share.textContent).toBe('Share')++ expect(share.dataset.ovShareSheet).toBe('')++ expect(share.dataset.url).toBe('https://openvibe.games/result/0%3A1700000000000-12')++ expect(share.dataset.title).toBe('A Salvage Rush round')++ expect(share.dataset.text).toBe(++ 'I scored 12 in Salvage Rush (rank 2 of 7). My best round: 12.',++ )expect(card.querySelector('.rush-result-board')?.getAttribute('href')).toBe('/leaderboard.html?game=salvage-rush',)@@@ -65,22 +65,16 @@ export function buildResultCard(result: RoundResult, origin: string): HTMLElemenconst actions = document.createElement('div')actions.className = 'rush-result-actions'++ // The network's one Share button, exactly as openvibe-shared 4.114.0's kit.share() builds it: the navbar loads++ // ov-share.js on the first press and opens it from these data-* attributes.const share = document.createElement('button')share.type = 'button'-- share.className = 'rush-result-share'-- share.textContent = 'Share'-- share.addEventListener('click', () => {-- const text = resultShareText(result)-- if (navigator.share) {-- navigator.share({ title: 'A Salvage Rush round', text, url }).catch(() => undefined)-- return-- }-- void navigator.clipboard?.writeText(`${text} ${url}`).catch(() => undefined)-- share.textContent = 'Copied'-- window.setTimeout(() => {-- share.textContent = 'Share'-- }, 1500)-- })++ share.className = 'ovk-btn ovk-btn--quiet rush-result-share'++ share.dataset.ovShareSheet = ''++ share.dataset.title = 'A Salvage Rush round'++ share.dataset.text = resultShareText(result)++ share.dataset.url = url++ share.innerHTML = '<svg class="ovk-i" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20V9M7 14l5-5 5 5M5 4h14"/></svg><span>Share</span>'const board = document.createElement('a')board.className = 'rush-result-board'board.href = '/leaderboard.html?game=salvage-rush'@@@ -52,8 +52,13 @@ describe('the result page of a round', () => {expect(resultPageUrl('https://openvibe.games/', '0:1700000000000-12')).toBe('https://openvibe.games/result/0%3A1700000000000-12',)-- expect(html).toContain('data-ovk-share')++ expect(html).toContain('data-ov-share-sheet')++ expect(html).toContain('data-url="https://openvibe.games/result/0%3A1700000000000-12"')++ expect(html).toContain('class="ovk-btn ovk-btn--quiet"')++ expect(html).toContain('data-title="A Salvage Rush round"')expect(html).toContain('I scored 12 in Salvage Rush')++ // The navbar is what loads ov-share.js on the first press, so the page must start it.++ expect(html).toContain('OpenVibeNavbar.init(')})it('carries the one line every game page carries', () => {@@@ -26,6 +26,7 @@ interface Kit {tool: Builderpage: Builderbutton: Builder++ share: Builderbadge: (text: string, tone?: string) => string}const sharedRequire = createRequire(import.meta.url)@@@ -98,13 +99,14 @@ export function resultPageHtml(input: ResultPageInput): string {]: [['Players', n(players)]],})-- const share = kit.button({-- href: url,++ const share = kit.share({++ title: 'A Salvage Rush round',++ url,++ text: shareText(input, mine),label: 'Share this result',-- icon: 'send',-- attrs: { 'data-ovk-share': shareText(input, mine) },})const body = [++ `<p>${share}</p>`,kit.tool({head: kit.toolHead({icon: 'trophy',@@@ -120,7 +122,6 @@ export function resultPageHtml(input: ResultPageInput): string {}),kit.section({body:-- `<div class="ovk-tool-card">${share}</div>` +`<p class="ovk-note">${kit.esc(GAME_PROMISE)}</p>` +kit.button({ href: '/leaderboard.html?game=salvage-rush', label: 'Full leaderboard', kind: 'ghost', icon: 'trophy' }),}),@@@ -133,7 +134,15 @@ export function resultPageHtml(input: ResultPageInput): string {}),body,})-- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>A Salvage Rush round · OpenVibe.Games</title><meta name="robots" content="noindex,follow"><meta name="description" content="A scored round of Salvage Rush on OpenVibe.Games."><script src="/shared/theme-loader.js"></script><link rel="stylesheet" href="/shared/kit.css"><link rel="stylesheet" href="/shared/kit-data.css"></head><body class="ovk-app"><div id="navbar-mount"></div>${html}<script src="/shared/navbar.js" defer></script><script src="/shared/kit-web.js" defer></script></body></html>`++ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>A Salvage Rush round · OpenVibe.Games</title><meta name="robots" content="noindex,follow"><meta name="description" content="A scored round of Salvage Rush on OpenVibe.Games."><style>body{margin:0}:where(body){font-family:var(--ov-font,'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif)}</style><script src="/shared/theme-loader.js"></script><link rel="stylesheet" href="/shared/kit.css"><link rel="stylesheet" href="/shared/kit-data.css"></head><body class="ovk-app"><div id="navbar-mount"></div>${html}<script src="/shared/web-runtime.js" defer></script><script src="/shared/navbar.js" defer></script><script src="/shared/kit-web.js" defer></script><script>window.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init(${JSON.stringify({++ service: 'games',++ apiBase: 'https://openvibe.network',++ links: [++ { label: 'Leaderboard', href: '/leaderboard.html' },++ { label: 'Play Scraplandia', href: '/play' },++ ],++ history: { type: 'page', title: 'A Salvage Rush round' },++ })}); });</script></body></html>`}export interface ResultRouteOptions {@@@ -142,6 +142,10 @@ describe('creator worlds in OpenVibe.Search', () => {expect(html).toContain('<link rel="canonical" href="https://openvibe.games/worlds/wk3j9x0a2bq"')expect(html).toContain('1 visit')expect(html).toContain('href="/worlds?remix=wk3j9x0a2bq"')++ expect(html).toContain('class="ovk-btn ovk-btn--quiet"')++ expect(html).toContain('data-ov-share-sheet')++ expect(html).toContain('data-url="https://openvibe.games/worlds/wk3j9x0a2bq"')++ expect(html).toContain('data-title="Moon <script>"')expect(html).not.toContain('Remix of')const remix = worldPageHtml({@@@ -14,6 +14,13 @@ interface Shell {page(o: Record<string, unknown>): string}++interface Kit {++ share(o: Record<string, unknown>): string++}++++const kit = (opts: { require?: NodeRequire } = {}) =>++ (opts.require ?? createRequire(import.meta.url))('openvibe-shared/kit') as Kit++const SITE_NAME = 'OpenVibe.Games'const NETWORK_URL = 'https://openvibe.network'const LINKS: NotFoundLink[] = [@@@ -33,7 +40,7 @@ const STYLE = `<style>.world .note{margin-top:22px;font-size:14px;color:var(--text-faint,#7386a3)}</style>`--export function worldPageHtml(world: CreatorWorld, siteUrl: string, shell: Shell): string {++export function worldPageHtml(world: CreatorWorld, siteUrl: string, shell: Shell, kitLib: Kit = kit()): string {const url = `${siteUrl}/worlds/${world.id}`const description =world.summary ||@@@ -56,7 +63,7 @@ export function worldPageHtml(world: CreatorWorld, siteUrl: string, shell: Shelldescription: description.slice(0, 300),canonical: url,robots: 'index, follow',-- head: `<link rel="icon" type="image/svg+xml" href="/favicon.svg" />\n${STYLE}`,++ head: `<link rel="icon" type="image/svg+xml" href="/favicon.svg" />\n<link rel="stylesheet" href="/shared/kit.css">\n${STYLE}`,navbar: {service: 'games',apiBase: NETWORK_URL,@@@ -82,6 +89,7 @@ export function worldPageHtml(world: CreatorWorld, siteUrl: string, shell: Shell<p class="stats">${escapeHtml(visits)}</p><div class="row"><a class="play" href="/play?world=${world.id}">▶ Join this world</a>++ ${kitLib.share({ title: world.title, url, text: description })}<a class="more" href="/worlds?remix=${world.id}">Remix it</a><a class="more" href="/worlds">More worlds</a></div>
@@ -6,6 +6,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
@@ -6,6 +6,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [Unreleased]
+### Added
+
+- Share: a creator world's page, a round's result page, the leaderboard and the in-game round result now open the network's one Share sheet (`data-ov-share-sheet`, openvibe-shared 4.114.0) with the page's public address. The result page now starts the shared navbar itself, which is what loads the sheet on the first press; `navigator.share` is gone.
+
### Fixed
### Fixed
- **A shared /worlds link had no preview.** The creator worlds page carried no Open Graph or Twitter tags, so a
- **A shared /worlds link had no preview.** The creator worlds page carried no Open Graph or Twitter tags, so a
@@ -13,6 +13,7 @@
@@ -13,6 +13,7 @@
/>
/>
<!-- The OpenVibe Frame: the user's theme before first paint, then navbar + footer. -->
<!-- The OpenVibe Frame: the user's theme before first paint, then navbar + footer. -->
<!-- Deferred: a slow or unreachable openvibe.network never holds the first paint (the inline colours above cover it). -->
<!-- Deferred: a slow or unreachable openvibe.network never holds the first paint (the inline colours above cover it). -->
+ <link rel="stylesheet" href="/shared/kit.css" />
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/theme-loader.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/shared/footer.js" defer></script>
<script src="/shared/footer.js" defer></script>
@@ -250,6 +250,16 @@
@@ -250,6 +250,16 @@
letter-spacing: 0.03em;
letter-spacing: 0.03em;
text-decoration: none;
text-decoration: none;
}
}
+ .cta-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px;
+ margin-top: 22px;
+ }
+ .cta-row .play {
+ margin-top: 0;
+ }
.note {
.note {
margin-top: 14px;
margin-top: 14px;
font-size: 14px;
font-size: 14px;
@@ -259,6 +269,7 @@
@@ -259,6 +269,7 @@
<script type="module" src="/src/pages/leaderboard.ts"></script>
<script type="module" src="/src/pages/leaderboard.ts"></script>
<!-- The OpenVibe Frame (navbar + footer), as on every other page of the site: this page had only its own row of
<!-- The OpenVibe Frame (navbar + footer), as on every other page of the site: this page had only its own row of
links (phone sweep, 2026-11-01). -->
links (phone sweep, 2026-11-01). -->
+ <link rel="stylesheet" href="/shared/kit.css" />
<script src="/shared/navbar.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/shared/footer.js" defer></script>
<script src="/shared/footer.js" defer></script>
<script>
<script>
@@ -311,7 +322,10 @@
@@ -311,7 +322,10 @@
><button id="next" type="button">Next</button>
><button id="next" type="button">Next</button>
</div>
</div>
</section>
</section>
- <a class="play" id="play-link" href="/play?game=salvage-rush">▶ Play a round</a>
+ <div class="cta-row">
+ <button class="ovk-btn ovk-btn--quiet" type="button" id="share" data-ov-share-sheet data-title="Salvage Rush leaderboard" data-url="https://openvibe.games/leaderboard.html?game=salvage-rush"><svg class="ovk-i" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20V9M7 14l5-5 5 5M5 4h14"/></svg><span>Share</span></button>
+ <a class="play" id="play-link" href="/play?game=salvage-rush">▶ Play a round</a>
+ </div>
<p class="note">
<p class="note">
"This week" starts on Monday, 00:00 UTC. These boards are kept by
"This week" starts on Monday, 00:00 UTC. These boards are kept by
<a href="https://play.openvibe.network/">OpenVibe.Play</a>, with the achievements you have
<a href="https://play.openvibe.network/">OpenVibe.Play</a>, with the achievements you have
@@ -31,6 +31,7 @@ const lede = document.getElementById('lede') as HTMLElement
@@ -31,6 +31,7 @@ const lede = document.getElementById('lede') as HTMLElement
const scoreHeader = document.getElementById('score-header') as HTMLElement
const scoreHeader = document.getElementById('score-header') as HTMLElement
const playLink = document.getElementById('play-link') as HTMLAnchorElement
const playLink = document.getElementById('play-link') as HTMLAnchorElement
const youLine = document.getElementById('you') as HTMLElement
const youLine = document.getElementById('you') as HTMLElement
+const shareButton = document.getElementById('share') as HTMLButtonElement
const COPY: Record<
const COPY: Record<
Game,
Game,
@@ -86,9 +87,20 @@ function cell(text: string, className?: string): HTMLTableCellElement {
@@ -86,9 +87,20 @@ function cell(text: string, className?: string): HTMLTableCellElement {
return td
return td
}
}
+/** The one Share sheet sends the board on screen: its game and period, in its public address. */
+function applyShare(): void {
+ const copy = COPY[game]
+ const query = new URLSearchParams({ game })
+ if (period === 'week') query.set('period', 'week')
+ shareButton.dataset.title = `${copy.title} leaderboard`
+ shareButton.dataset.text = copy.lede
+ shareButton.dataset.url = `https://openvibe.games/leaderboard.html?${query.toString()}`
+}
+
function applyGame(): void {
function applyGame(): void {
for (const tab of gameTabs) tab.setAttribute('aria-pressed', String(tab.dataset.game === game))
for (const tab of gameTabs) tab.setAttribute('aria-pressed', String(tab.dataset.game === game))
const copy = COPY[game]
const copy = COPY[game]
+ applyShare()
title.textContent = copy.title
title.textContent = copy.title
lede.textContent = copy.lede
lede.textContent = copy.lede
scoreHeader.textContent = copy.score
scoreHeader.textContent = copy.score
@@ -123,6 +135,7 @@ function markMine(mine: Mine | null): void {
@@ -123,6 +135,7 @@ function markMine(mine: Mine | null): void {
}
}
async function load(): Promise<void> {
async function load(): Promise<void> {
+ applyShare()
for (const tab of periodTabs)
for (const tab of periodTabs)
tab.setAttribute('aria-pressed', String(tab.dataset.period === period))
tab.setAttribute('aria-pressed', String(tab.dataset.period === period))
const copy = COPY[game]
const copy = COPY[game]
@@ -67,6 +67,9 @@
@@ -67,6 +67,9 @@
.rush-result-actions > * {
.rush-result-actions > * {
flex: 1 1 8rem;
flex: 1 1 8rem;
min-height: 2.75rem;
min-height: 2.75rem;
+}
+.rush-result-actions > :not(.ovk-btn) {
+ min-height: 2.75rem;
display: inline-flex;
display: inline-flex;
align-items: center;
align-items: center;
justify-content: center;
justify-content: center;
@@ -81,11 +84,6 @@
@@ -81,11 +84,6 @@
text-decoration: none;
text-decoration: none;
cursor: pointer;
cursor: pointer;
}
}
-.rush-result-share {
- background: var(--accent-strong, var(--accent));
- border-color: transparent;
- color: var(--on-accent-strong, var(--on-accent, #fff));
-}
.rush-result-board {
.rush-result-board {
background: transparent;
background: transparent;
}
}
@@ -39,7 +39,15 @@ describe('the result card of a round', () => {
@@ -39,7 +39,15 @@ describe('the result card of a round', () => {
it('carries the buttons a player needs', () => {
it('carries the buttons a player needs', () => {
const card = buildResultCard(result, 'https://openvibe.games')
const card = buildResultCard(result, 'https://openvibe.games')
- expect(card.querySelector('.rush-result-share')?.textContent).toBe('Share')
+ const share = card.querySelector('.rush-result-share') as HTMLButtonElement
+ expect(share.className).toContain('ovk-btn ovk-btn--quiet')
+ expect(share.textContent).toBe('Share')
+ expect(share.dataset.ovShareSheet).toBe('')
+ expect(share.dataset.url).toBe('https://openvibe.games/result/0%3A1700000000000-12')
+ expect(share.dataset.title).toBe('A Salvage Rush round')
+ expect(share.dataset.text).toBe(
+ 'I scored 12 in Salvage Rush (rank 2 of 7). My best round: 12.',
+ )
expect(card.querySelector('.rush-result-board')?.getAttribute('href')).toBe(
expect(card.querySelector('.rush-result-board')?.getAttribute('href')).toBe(
'/leaderboard.html?game=salvage-rush',
'/leaderboard.html?game=salvage-rush',
)
)
@@ -65,22 +65,16 @@ export function buildResultCard(result: RoundResult, origin: string): HTMLElemen
@@ -65,22 +65,16 @@ export function buildResultCard(result: RoundResult, origin: string): HTMLElemen
const actions = document.createElement('div')
const actions = document.createElement('div')
actions.className = 'rush-result-actions'
actions.className = 'rush-result-actions'
+ // The network's one Share button, exactly as openvibe-shared 4.114.0's kit.share() builds it: the navbar loads
+ // ov-share.js on the first press and opens it from these data-* attributes.
const share = document.createElement('button')
const share = document.createElement('button')
share.type = 'button'
share.type = 'button'
- share.className = 'rush-result-share'
- share.textContent = 'Share'
- share.addEventListener('click', () => {
- const text = resultShareText(result)
- if (navigator.share) {
- navigator.share({ title: 'A Salvage Rush round', text, url }).catch(() => undefined)
- return
- }
- void navigator.clipboard?.writeText(`${text} ${url}`).catch(() => undefined)
- share.textContent = 'Copied'
- window.setTimeout(() => {
- share.textContent = 'Share'
- }, 1500)
- })
+ share.className = 'ovk-btn ovk-btn--quiet rush-result-share'
+ share.dataset.ovShareSheet = ''
+ share.dataset.title = 'A Salvage Rush round'
+ share.dataset.text = resultShareText(result)
+ share.dataset.url = url
+ share.innerHTML = '<svg class="ovk-i" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20V9M7 14l5-5 5 5M5 4h14"/></svg><span>Share</span>'
const board = document.createElement('a')
const board = document.createElement('a')
board.className = 'rush-result-board'
board.className = 'rush-result-board'
board.href = '/leaderboard.html?game=salvage-rush'
board.href = '/leaderboard.html?game=salvage-rush'
@@ -52,8 +52,13 @@ describe('the result page of a round', () => {
@@ -52,8 +52,13 @@ describe('the result page of a round', () => {
expect(resultPageUrl('https://openvibe.games/', '0:1700000000000-12')).toBe(
expect(resultPageUrl('https://openvibe.games/', '0:1700000000000-12')).toBe(
'https://openvibe.games/result/0%3A1700000000000-12',
'https://openvibe.games/result/0%3A1700000000000-12',
)
)
- expect(html).toContain('data-ovk-share')
+ expect(html).toContain('data-ov-share-sheet')
+ expect(html).toContain('data-url="https://openvibe.games/result/0%3A1700000000000-12"')
+ expect(html).toContain('class="ovk-btn ovk-btn--quiet"')
+ expect(html).toContain('data-title="A Salvage Rush round"')
expect(html).toContain('I scored 12 in Salvage Rush')
expect(html).toContain('I scored 12 in Salvage Rush')
+ // The navbar is what loads ov-share.js on the first press, so the page must start it.
+ expect(html).toContain('OpenVibeNavbar.init(')
})
})
it('carries the one line every game page carries', () => {
it('carries the one line every game page carries', () => {
@@ -26,6 +26,7 @@ interface Kit {
@@ -26,6 +26,7 @@ interface Kit {
tool: Builder
tool: Builder
page: Builder
page: Builder
button: Builder
button: Builder
+ share: Builder
badge: (text: string, tone?: string) => string
badge: (text: string, tone?: string) => string
}
}
const sharedRequire = createRequire(import.meta.url)
const sharedRequire = createRequire(import.meta.url)
@@ -98,13 +99,14 @@ export function resultPageHtml(input: ResultPageInput): string {
@@ -98,13 +99,14 @@ export function resultPageHtml(input: ResultPageInput): string {
]
]
: [['Players', n(players)]],
: [['Players', n(players)]],
})
})
- const share = kit.button({
- href: url,
+ const share = kit.share({
+ title: 'A Salvage Rush round',
+ url,
+ text: shareText(input, mine),
label: 'Share this result',
label: 'Share this result',
- icon: 'send',
- attrs: { 'data-ovk-share': shareText(input, mine) },
})
})
const body = [
const body = [
+ `<p>${share}</p>`,
kit.tool({
kit.tool({
head: kit.toolHead({
head: kit.toolHead({
icon: 'trophy',
icon: 'trophy',
@@ -120,7 +122,6 @@ export function resultPageHtml(input: ResultPageInput): string {
@@ -120,7 +122,6 @@ export function resultPageHtml(input: ResultPageInput): string {
}),
}),
kit.section({
kit.section({
body:
body:
- `<div class="ovk-tool-card">${share}</div>` +
`<p class="ovk-note">${kit.esc(GAME_PROMISE)}</p>` +
`<p class="ovk-note">${kit.esc(GAME_PROMISE)}</p>` +
kit.button({ href: '/leaderboard.html?game=salvage-rush', label: 'Full leaderboard', kind: 'ghost', icon: 'trophy' }),
kit.button({ href: '/leaderboard.html?game=salvage-rush', label: 'Full leaderboard', kind: 'ghost', icon: 'trophy' }),
}),
}),
@@ -133,7 +134,15 @@ export function resultPageHtml(input: ResultPageInput): string {
@@ -133,7 +134,15 @@ export function resultPageHtml(input: ResultPageInput): string {
}),
}),
body,
body,
})
})
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>A Salvage Rush round · OpenVibe.Games</title><meta name="robots" content="noindex,follow"><meta name="description" content="A scored round of Salvage Rush on OpenVibe.Games."><script src="/shared/theme-loader.js"></script><link rel="stylesheet" href="/shared/kit.css"><link rel="stylesheet" href="/shared/kit-data.css"></head><body class="ovk-app"><div id="navbar-mount"></div>${html}<script src="/shared/navbar.js" defer></script><script src="/shared/kit-web.js" defer></script></body></html>`
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>A Salvage Rush round · OpenVibe.Games</title><meta name="robots" content="noindex,follow"><meta name="description" content="A scored round of Salvage Rush on OpenVibe.Games."><style>body{margin:0}:where(body){font-family:var(--ov-font,'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif)}</style><script src="/shared/theme-loader.js"></script><link rel="stylesheet" href="/shared/kit.css"><link rel="stylesheet" href="/shared/kit-data.css"></head><body class="ovk-app"><div id="navbar-mount"></div>${html}<script src="/shared/web-runtime.js" defer></script><script src="/shared/navbar.js" defer></script><script src="/shared/kit-web.js" defer></script><script>window.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init(${JSON.stringify({
+ service: 'games',
+ apiBase: 'https://openvibe.network',
+ links: [
+ { label: 'Leaderboard', href: '/leaderboard.html' },
+ { label: 'Play Scraplandia', href: '/play' },
+ ],
+ history: { type: 'page', title: 'A Salvage Rush round' },
+ })}); });</script></body></html>`
}
}
export interface ResultRouteOptions {
export interface ResultRouteOptions {
@@ -142,6 +142,10 @@ describe('creator worlds in OpenVibe.Search', () => {
@@ -142,6 +142,10 @@ describe('creator worlds in OpenVibe.Search', () => {
expect(html).toContain('<link rel="canonical" href="https://openvibe.games/worlds/wk3j9x0a2bq"')
expect(html).toContain('<link rel="canonical" href="https://openvibe.games/worlds/wk3j9x0a2bq"')
expect(html).toContain('1 visit')
expect(html).toContain('1 visit')
expect(html).toContain('href="/worlds?remix=wk3j9x0a2bq"')
expect(html).toContain('href="/worlds?remix=wk3j9x0a2bq"')
+ expect(html).toContain('class="ovk-btn ovk-btn--quiet"')
+ expect(html).toContain('data-ov-share-sheet')
+ expect(html).toContain('data-url="https://openvibe.games/worlds/wk3j9x0a2bq"')
+ expect(html).toContain('data-title="Moon <script>"')
expect(html).not.toContain('Remix of')
expect(html).not.toContain('Remix of')
const remix = worldPageHtml(
const remix = worldPageHtml(
{
{
@@ -14,6 +14,13 @@ interface Shell {
@@ -14,6 +14,13 @@ interface Shell {
page(o: Record<string, unknown>): string
page(o: Record<string, unknown>): string
}
}
+interface Kit {
+ share(o: Record<string, unknown>): string
+}
+
+const kit = (opts: { require?: NodeRequire } = {}) =>
+ (opts.require ?? createRequire(import.meta.url))('openvibe-shared/kit') as Kit
+
const SITE_NAME = 'OpenVibe.Games'
const SITE_NAME = 'OpenVibe.Games'
const NETWORK_URL = 'https://openvibe.network'
const NETWORK_URL = 'https://openvibe.network'
const LINKS: NotFoundLink[] = [
const LINKS: NotFoundLink[] = [
@@ -33,7 +40,7 @@ const STYLE = `<style>
@@ -33,7 +40,7 @@ const STYLE = `<style>
.world .note{margin-top:22px;font-size:14px;color:var(--text-faint,#7386a3)}
.world .note{margin-top:22px;font-size:14px;color:var(--text-faint,#7386a3)}
</style>`
</style>`
-export function worldPageHtml(world: CreatorWorld, siteUrl: string, shell: Shell): string {
+export function worldPageHtml(world: CreatorWorld, siteUrl: string, shell: Shell, kitLib: Kit = kit()): string {
const url = `${siteUrl}/worlds/${world.id}`
const url = `${siteUrl}/worlds/${world.id}`
const description =
const description =
world.summary ||
world.summary ||
@@ -56,7 +63,7 @@ export function worldPageHtml(world: CreatorWorld, siteUrl: string, shell: Shell
@@ -56,7 +63,7 @@ export function worldPageHtml(world: CreatorWorld, siteUrl: string, shell: Shell
description: description.slice(0, 300),
description: description.slice(0, 300),
canonical: url,
canonical: url,
robots: 'index, follow',
robots: 'index, follow',
- head: `<link rel="icon" type="image/svg+xml" href="/favicon.svg" />\n${STYLE}`,
+ head: `<link rel="icon" type="image/svg+xml" href="/favicon.svg" />\n<link rel="stylesheet" href="/shared/kit.css">\n${STYLE}`,
navbar: {
navbar: {
service: 'games',
service: 'games',
apiBase: NETWORK_URL,
apiBase: NETWORK_URL,
@@ -82,6 +89,7 @@ export function worldPageHtml(world: CreatorWorld, siteUrl: string, shell: Shell
@@ -82,6 +89,7 @@ export function worldPageHtml(world: CreatorWorld, siteUrl: string, shell: Shell
<p class="stats">${escapeHtml(visits)}</p>
<p class="stats">${escapeHtml(visits)}</p>
<div class="row">
<div class="row">
<a class="play" href="/play?world=${world.id}">▶ Join this world</a>
<a class="play" href="/play?world=${world.id}">▶ Join this world</a>
+ ${kitLib.share({ title: world.title, url, text: description })}
<a class="more" href="/worlds?remix=${world.id}">Remix it</a>
<a class="more" href="/worlds?remix=${world.id}">Remix it</a>
<a class="more" href="/worlds">More worlds</a>
<a class="more" href="/worlds">More worlds</a>
</div>
</div>