From 93db68c2942201e66c3ec22ed1b14c240ea41e38 Mon Sep 17 00:00:00 2001 From: Rishab Dugar Date: Thu, 8 Oct 2026 05:32:56 +0000 Subject: [PATCH] feat: share Fizzdoc with a friend A share button in the header opens the phone's own share sheet, or a small menu of links on desktop: WhatsApp, X, Facebook, LinkedIn, Reddit, Telegram, email and copy link. The same links sit under the star call-out on every page and after each result. They are plain links, so nothing loads from those sites until someone clicks. On the narrowest phones the theme switch steps aside to make room; the theme already follows the device. --- index.html | 11 ++++ public/third-party-licenses.txt | 1 + src/effects.ts | 51 +++++++++++++++++ src/i18n.ts | 8 +++ src/i18n/bn.json | 8 +++ src/i18n/de.json | 8 +++ src/i18n/es.json | 8 +++ src/i18n/fr.json | 8 +++ src/i18n/hi.json | 8 +++ src/i18n/id.json | 8 +++ src/i18n/it.json | 8 +++ src/i18n/mr.json | 8 +++ src/i18n/nl.json | 8 +++ src/i18n/pl.json | 8 +++ src/i18n/pt.json | 8 +++ src/i18n/ta.json | 8 +++ src/i18n/te.json | 8 +++ src/i18n/tr.json | 8 +++ src/i18n/vi.json | 8 +++ src/seo.ts | 6 +- src/share.ts | 34 ++++++++++++ src/style.css | 97 ++++++++++++++++++++++++++++++++- tests/app.e2e.ts | 33 +++++++++++ 23 files changed, 357 insertions(+), 4 deletions(-) create mode 100644 src/share.ts diff --git a/index.html b/index.html index d117229..0234c86 100644 --- a/index.html +++ b/index.html @@ -28,6 +28,13 @@ {{t:format.audio}} {{t:nav.star}} +
+ + +
`, + ); + return ``; +} diff --git a/src/style.css b/src/style.css index 83bef89..dc157e0 100644 --- a/src/style.css +++ b/src/style.css @@ -234,7 +234,7 @@ main { align-items: center; gap: 2px; } -.site-header nav a { +.site-header nav > a { padding: 6px 10px; border-radius: 8px; color: var(--muted); @@ -243,7 +243,7 @@ main { text-decoration: none; transition: color 0.15s, background-color 0.15s; } -.site-header nav a:hover { +.site-header nav > a:hover { color: var(--text); background: var(--surface-2); } @@ -345,7 +345,7 @@ main { } @media (max-width: 900px) { - .site-header nav a:not(.star-btn) { + .site-header nav > a:not(.star-btn) { display: none; } } @@ -355,6 +355,29 @@ main { display: none; } } +/* Room for the share button next to the theme switch on narrow phones. */ +@media (max-width: 420px) { + .star-btn > svg:first-child { + display: none; + } + .site-header .lang select { + max-width: 84px; + } +} +/* The smallest phones: the theme already follows the device, so its switch gives way to sharing. */ +@media (max-width: 380px) { + .site-header .theme-toggle:not(.share-btn) { + display: none; + } +} +@media (max-width: 340px) { + .site-header .bar { + gap: 8px; + } + .site-header .lang select { + max-width: 70px; + } +} /* ---------- Badges & chips ---------- */ @@ -1776,3 +1799,71 @@ dialog p { color: var(--muted); font-size: 0.8rem; } + +/* Share with a friend: header menu, the box under the star call-out, and the line after a result. */ +.share { + position: relative; +} +.share-pop { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 40; + width: min(300px, calc(100vw - 32px)); + padding: 14px; + border: 1px solid var(--line); + border-radius: 16px; + background: var(--surface); + box-shadow: var(--shadow-md); +} +.share-pop p { + margin: 0 0 10px; + font-size: 0.9rem; + color: var(--muted); +} +.share-links { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.share-link { + display: inline-grid; + place-items: center; + width: 40px; + height: 40px; + padding: 0; + border: 1px solid var(--line); + border-radius: 50%; + background: var(--surface-2, var(--surface)); + color: var(--text); + cursor: pointer; + transition: transform 0.15s, border-color 0.15s; +} +.share-link svg { + width: 18px; + height: 18px; + fill: currentColor; +} +.share-link:hover, +.share-link:focus-visible { + transform: translateY(-2px); + border-color: var(--line-strong); +} +.share-link.done { + border-color: #1d9a5b; + color: #1d9a5b; +} +.share-cta { + flex-basis: 100%; + padding-top: 20px; + border-top: 1px solid var(--line); +} +.share-cta p, +.share-nudge p { + margin: 0 0 10px; + color: var(--muted); + font-size: 0.92rem; +} +.share-nudge { + margin-top: 14px; +} diff --git a/tests/app.e2e.ts b/tests/app.e2e.ts index 8c79134..21d61ba 100644 --- a/tests/app.e2e.ts +++ b/tests/app.e2e.ts @@ -1026,3 +1026,36 @@ test('publishes sitemap, robots.txt and llms.txt', async ({ request }) => { expect(await (await request.get('/robots.txt')).text()).toContain('User-agent: *\nAllow: /'); expect((await request.get('/og.png')).headers()['content-type']).toBe('image/png'); }); + +test('shares Fizzdoc: the phone’s share sheet, or links to the big networks and a copy button', async ({ browser }) => { + const context = await browser.newContext({ permissions: ['clipboard-read', 'clipboard-write'] }); + const page = await context.newPage(); + await page.goto('/merge-pdf/'); + await page.getByRole('button', { name: 'Share Fizzdoc' }).click(); + const menu = page.locator('#share-pop'); + await expect(menu).toBeVisible(); + const whatsapp = menu.getByRole('link', { name: 'Share on WhatsApp' }); + await expect(whatsapp).toHaveAttribute('href', /^https:\/\/wa\.me\/\?text=Merge%20PDF.*fizzdoc\.com%2Fmerge-pdf%2F$/); + await expect(menu.getByRole('link', { name: 'Share on LinkedIn' })).toHaveAttribute('href', 'https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Ffizzdoc.com%2Fmerge-pdf%2F'); + await menu.getByRole('button', { name: 'Copy link' }).click(); + await expect(menu.getByRole('button', { name: 'Link copied' })).toBeVisible(); + expect(await page.evaluate(() => navigator.clipboard.readText())).toBe('https://fizzdoc.com/merge-pdf/'); + await page.keyboard.press('Escape'); + await expect(menu).toBeHidden(); + // The same links sit under the star call-out on every page. + await expect(page.locator('.share-cta .share-link')).toHaveCount(8); + await context.close(); + + // On a phone the system share sheet opens instead of the menu. + const phone = await browser.newContext({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true }); + const mobile = await phone.newPage(); + await mobile.addInitScript(() => { + Object.defineProperty(navigator, 'share', { value: async (data: ShareData) => ((window as unknown as { shared: ShareData }).shared = data) }); + }); + await mobile.goto('/hi/'); + await mobile.locator('#share-btn').tap(); + expect(await mobile.evaluate(() => (window as unknown as { shared: ShareData }).shared.url)).toBe('https://fizzdoc.com/hi/'); + await expect(mobile.locator('#share-pop')).toBeHidden(); + expect(await mobile.evaluate(() => document.documentElement.scrollWidth)).toBe(390); + await phone.close(); +});