web: share the landing page to social networks with a real preview card
Open Graph and Twitter Card meta point at a 1200x630 cover shot of the hero, and the final CTA grows a share row: Facebook, X, LinkedIn and Telegram each open with this origin filled in, plus a native share sheet that falls back to copying the link. The top bar now leads with the web studio pill next to the store CTA, with the theme, language and account controls trailing them.
This commit is contained in:
@@ -237,6 +237,37 @@ export function Landing() {
|
||||
const toastDownload: Txt = { en: 'Download links land with the public release — the web studio is live now.', vi: 'Link tải sẽ có khi phát hành chính thức — web studio đã chạy được ngay.' };
|
||||
const toastStore: Txt = { en: 'Store listing arrives with the public release — use the web studio in the meantime.', vi: 'Cửa hàng ứng dụng sẽ mở khi phát hành chính thức — trong lúc chờ, hãy dùng web studio.' };
|
||||
|
||||
// Sharing. The URL is the page's own origin, so the links point at whatever
|
||||
// host is serving this build (the crawler meta in index.html does the same
|
||||
// with relative URLs). Each network opens in a new tab with the URL already
|
||||
// filled in; the last control uses the OS share sheet when there is one and
|
||||
// copies the link when there is not.
|
||||
const shareUrl = `${window.location.origin}/`;
|
||||
const shareText = c({
|
||||
en: 'RecipesCam — Kodak and Fuji film colour on your own photos, on the phone and in the browser.',
|
||||
vi: 'RecipesCam — màu phim Kodak và Fuji trên ảnh của bạn, trên điện thoại và trên trình duyệt.',
|
||||
});
|
||||
const shareLinks: { id: string; label: string; href: string }[] = [
|
||||
{ id: 'facebook', label: 'FACEBOOK', href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}` },
|
||||
{ id: 'x', label: 'X', href: `https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(shareText)}` },
|
||||
{ id: 'linkedin', label: 'LINKEDIN', href: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}` },
|
||||
{ id: 'telegram', label: 'TELEGRAM', href: `https://t.me/share/url?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(shareText)}` },
|
||||
];
|
||||
const share = async () => {
|
||||
if (navigator.share) {
|
||||
// Dismissing the sheet is not a failure — say nothing.
|
||||
await navigator.share({ title: 'RecipesCam', text: shareText, url: shareUrl }).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
showToast(c({ en: 'Link copied to the clipboard.', vi: 'Đã sao chép liên kết.' }));
|
||||
} catch {
|
||||
// No clipboard (an insecure origin): hand the URL over in the toast.
|
||||
showToast(c({ en: `Copy this link: ${shareUrl}`, vi: `Sao chép liên kết này: ${shareUrl}` }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="lp" data-lang={lang}>
|
||||
{/* 1 — fixed glass navigation */}
|
||||
@@ -252,6 +283,14 @@ export function Landing() {
|
||||
{nav.map((entry) => <a key={entry.href} href={entry.href}>{c(entry.label)}</a>)}
|
||||
</div>
|
||||
<span className="lp-spacer" />
|
||||
{/* The browser studio is live today, so it leads the row and wears
|
||||
the same primary pill as the store CTA. */}
|
||||
<a className="lp-btn lp-sm lp-primary" data-key="nav-studio" href="/app">
|
||||
{c({ en: 'RecipesCam web studio', vi: 'Studio web RecipesCam' })}
|
||||
</a>
|
||||
<button type="button" className="lp-btn lp-sm lp-primary" data-key="nav-download" onClick={() => showToast(toastDownload)}>
|
||||
{c({ en: 'Download RecipesCam', vi: 'Tải RecipesCam' })}
|
||||
</button>
|
||||
{/* Theme + language, the same two switches the workspace TopBar has.
|
||||
Theme opens a popover: light/dark plus the font group, which
|
||||
retypes the whole page. */}
|
||||
@@ -348,14 +387,6 @@ export function Landing() {
|
||||
{t('nav.login')}
|
||||
</button>
|
||||
)}
|
||||
{/* Between the account slot and the store CTA: the browser studio is
|
||||
live today, so it gets its own way in from the top bar. */}
|
||||
<a className="lp-btn lp-sm" data-key="nav-studio" href="/app">
|
||||
{c({ en: 'RecipesCam web studio', vi: 'Studio web RecipesCam' })}
|
||||
</a>
|
||||
<button type="button" className="lp-btn lp-sm lp-primary" onClick={() => showToast(toastDownload)}>
|
||||
{c({ en: 'Download RecipesCam', vi: 'Tải RecipesCam' })}
|
||||
</button>
|
||||
<button type="button" className="lp-burger" aria-label="menu" aria-expanded={menu} onClick={() => setMenu((v) => !v)}>
|
||||
<i /><i /><i />
|
||||
</button>
|
||||
@@ -381,6 +412,7 @@ export function Landing() {
|
||||
<button
|
||||
type="button"
|
||||
className="lp-btn lp-primary"
|
||||
data-key="sheet-download"
|
||||
onClick={() => { setMenu(false); showToast(toastDownload); }}
|
||||
>
|
||||
{c({ en: 'Download RecipesCam', vi: 'Tải RecipesCam' })}
|
||||
@@ -718,6 +750,26 @@ export function Landing() {
|
||||
</button>
|
||||
<a className="lp-btn lp-primary" href="/app">{c({ en: 'Open the Web Studio', vi: 'Mở Web Studio' })}</a>
|
||||
</div>
|
||||
{/* Share targets: the four networks take the URL as a query param,
|
||||
the last control uses the OS sheet (phones) or the clipboard. */}
|
||||
<div className="lp-share" data-key="share-row">
|
||||
<span className="lp-share-label">{c({ en: 'SHARE THE PAGE', vi: 'CHIA SẺ TRANG' })}</span>
|
||||
{shareLinks.map((s) => (
|
||||
<a
|
||||
key={s.id}
|
||||
className="lp-btn lp-sm"
|
||||
data-key={`share-${s.id}`}
|
||||
href={s.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{s.label}
|
||||
</a>
|
||||
))}
|
||||
<button type="button" className="lp-btn lp-sm" data-key="share-native" onClick={() => void share()}>
|
||||
{c({ en: 'SHARE…', vi: 'CHIA SẺ…' })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -424,6 +424,9 @@ html { scroll-behavior: smooth; scroll-padding-top: 78px; }
|
||||
.lp-store { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.25; }
|
||||
.lp-store small { font-family: var(--lp-mono); font-size: 9.5px; color: var(--lp-muted); }
|
||||
.lp-store b { font-size: 15px; }
|
||||
/* Share row under the store badges: the label reads as a mono caption. */
|
||||
.lp-share { display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; margin-top: 26px; }
|
||||
.lp-share-label { font-family: var(--lp-mono); font-size: 10px; letter-spacing: 0.12em; color: var(--lp-muted); margin-right: 4px; }
|
||||
.lp-footer { border-top: 1px solid var(--lp-line); padding: 30px 0 40px; }
|
||||
.lp-footer .lp-wrap { display: flex; flex-wrap: wrap; gap: 14px 26px; align-items: center; font-size: 12.5px; color: var(--lp-muted); }
|
||||
.lp-footer a:hover { color: var(--lp-text); }
|
||||
|
||||
Reference in New Issue
Block a user