Compare commits

..

12 Commits

Author SHA1 Message Date
b8103ea072 1
All checks were successful
Deploy to talkpro / build-and-sync (push) Successful in 27s
2026-05-18 15:23:09 +08:00
a481c382a6 CI deploy without sudo: tar over ssh when rsync missing
All checks were successful
Deploy to talkpro / build-and-sync (push) Successful in 33s
Remove apt/sudo install step (act runners lack sudo). Use rsync when
available; otherwise clear remote web root and stream dist/ via tar|ssh.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:19:16 +08:00
5f9e8db7e5 CI: install rsync and openssh-client before deploy
Some checks failed
Deploy to talkpro / build-and-sync (push) Failing after 11s
Gitea/act runners often lack rsync; apt/apk/dnf install step runs
before ssh/rsync in deploy-talkpro.sh.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:17:13 +08:00
fe14ca30ff Fix deploy script: use SSH_OPTS array for ssh mkdir
Some checks failed
Deploy to talkpro / build-and-sync (push) Failing after 31s
Typo SSH[@] left ssh out of the command so the shell tried to run
ubuntu@host as a program (exit 127).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:15:31 +08:00
cf9cbb6134 Fix CI: remove optional secret overrides step
Some checks failed
Deploy to talkpro / build-and-sync (push) Failing after 31s
Empty optional secrets made [ -n ... ] && echo fail under set -e.
Job env already defines host, user, and deploy path.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:13:48 +08:00
77485eee63 Fix CI deploy: default talkpro host, require only SSH secret
Some checks failed
Deploy to talkpro / build-and-sync (push) Failing after 14s
Gitea workflow no longer needs TALKPRO_HOST secret; defaults match
talkpro VPS. Fail fast with a clear message if TALKPRO_SSH_PRIVATE_KEY
is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:06:14 +08:00
6e90a4adb6 Add Gitea Actions deploy workflow for talkpro.info
Some checks failed
Deploy to talkpro / build-and-sync (push) Failing after 18s
Build Astro on push to main/master and rsync dist/ to the marketing
server via SSH. Includes deploy-talkpro.sh, env example, and README.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 15:01:58 +08:00
3933cf42c0 1 2026-05-18 14:58:09 +08:00
df6cff4895 Merge pull request 'fix(style): Changes responsive layout breakpoint' (#8) from finn-staging into main
Reviewed-on: #8
2026-05-18 06:41:21 +00:00
82e3a23df3 Merge pull request 'fix(feat): Refine responsive layout and section spacing' (#7) from finn-staging into main
Reviewed-on: #7
2026-05-15 09:46:01 +00:00
66f52a2d6e Merge pull request 'fix(feat): add locales and refine responsive landing page UI' (#6) from finn-staging into main
Reviewed-on: #6
2026-05-15 05:41:46 +00:00
b6e6178466 Merge pull request 'finn-staging' (#5) from finn-staging into main
Reviewed-on: #5
2026-05-14 10:02:48 +00:00
13 changed files with 599 additions and 31 deletions

10
.env.deploy.example Normal file
View File

@@ -0,0 +1,10 @@
# Copy to .env.deploy for local runs: set -a && source .env.deploy && set +a && ./scripts/deploy-talkpro.sh
#
# Gitea Actions: only this secret is required (Settings → Secrets → Actions):
# TALKPRO_SSH_PRIVATE_KEY = full PEM file contents
# Host/user/path defaults are in .gitea/workflows/deploy-talkpro.yml
TALKPRO_HOST=13.214.179.69
TALKPRO_USER=ubuntu
TALKPRO_REMOTE_ROOT=/home/ubuntu/talkpro
TALKPRO_SSH_KEY_FILE=/absolute/path/to/luis-only.pem

View File

@@ -0,0 +1,57 @@
# Build talk-pro and rsync dist/ to the marketing VPS (talkpro.info).
#
# Required Gitea repo secret (Settings → Secrets → Actions):
# TALKPRO_SSH_PRIVATE_KEY full PEM for ubuntu@talkpro (same as luis-only.pem)
#
# Host/user/path defaults are in the `env:` block below (edit there if needed).
#
# Requires a runner with: node 22+, ssh, ssh-keyscan, tar (rsync optional; no sudo needed).
name: Deploy to talkpro
on:
push:
branches:
- main
- master
workflow_dispatch:
env:
TALKPRO_HOST: "13.214.179.69"
TALKPRO_USER: ubuntu
TALKPRO_REMOTE_ROOT: /home/ubuntu/talkpro
jobs:
build-and-sync:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
- name: Check deploy secrets
env:
TALKPRO_SSH_PRIVATE_KEY: ${{ secrets.TALKPRO_SSH_PRIVATE_KEY }}
run: |
if [ -z "${TALKPRO_SSH_PRIVATE_KEY}" ]; then
echo "ERROR: Missing Gitea secret TALKPRO_SSH_PRIVATE_KEY"
echo "Add it under Repository → Settings → Secrets (Actions)."
echo "Value: full contents of your ubuntu@talkpro SSH private key (PEM)."
exit 1
fi
- name: Trust host key
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keyscan -H "$TALKPRO_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
- name: Build and deploy to talkpro
env:
TALKPRO_SSH_PRIVATE_KEY: ${{ secrets.TALKPRO_SSH_PRIVATE_KEY }}
run: bash scripts/deploy-talkpro.sh

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@ dist/
.env .env
.env.* .env.*
!.env.example !.env.example
!.env.deploy.example
.env.deploy

View File

@@ -26,6 +26,28 @@ Open [http://localhost:4321](http://localhost:4321)
| `npm run build` | Build for production into `dist/` | | `npm run build` | Build for production into `dist/` |
| `npm run preview` | Preview production build locally | | `npm run preview` | Preview production build locally |
## Deploy (Gitea Actions → talkpro.info)
Pushes to **`main`** or **`master`** run [`.gitea/workflows/deploy-talkpro.yml`](.gitea/workflows/deploy-talkpro.yml): `npm ci``npm run build``rsync` `dist/` to the marketing server (`/home/ubuntu/talkpro`).
**Repository secret** (Gitea → Settings → Secrets → Actions) — **required**:
| Secret | Value |
|--------|--------|
| `TALKPRO_SSH_PRIVATE_KEY` | Full PEM text of the `ubuntu@talkpro` deploy key |
Host (`13.214.179.69`), user (`ubuntu`), and web root (`/home/ubuntu/talkpro`) are set in the workflow. Optional secrets `TALKPRO_HOST`, `TALKPRO_USER`, `TALKPRO_REMOTE_ROOT` override those defaults.
Manual deploy from this repo:
```bash
cp .env.deploy.example .env.deploy # edit paths
set -a && source .env.deploy && set +a
bash scripts/deploy-talkpro.sh
```
`/api/site-links` (APK / App Store URLs) is still updated via the parent **talkpro** repo: `./scripts/post-talkpro-site-links.sh` on your laptop.
## Project Structure ## Project Structure
``` ```

310
public/site-links-client.js Normal file
View File

@@ -0,0 +1,310 @@
/**
* Mirrors talkpro static site: /api/site-links, APK default, in-app + App Store modals.
* Strings come from #site-links-i18n (JSON) rendered by DownloadCTA.astro.
*/
(function () {
const DEFAULT_APK = 'https://talkspro.xyz/download';
let apkDownloadUrl = DEFAULT_APK;
function getApkDownloadUrl() {
return apkDownloadUrl;
}
function getI18n() {
const el = document.getElementById('site-links-i18n');
if (!el || !el.textContent.trim()) return {};
try {
return JSON.parse(el.textContent);
} catch {
return {};
}
}
function detectInAppBrowser() {
const ua = navigator.userAgent || '';
if (/MicroMessenger/i.test(ua)) return 'wechat';
try {
if (typeof window.WeixinJSBridge !== 'undefined') return 'wechat';
} catch {
/* ignore */
}
if (/QQ\//i.test(ua) && /MQQBrowser/i.test(ua)) return 'qq';
if (/Weibo/i.test(ua)) return 'weibo';
return '';
}
function isIOSDevice() {
const ua = navigator.userAgent || '';
if (/iPhone|iPad|iPod/i.test(ua)) return true;
if (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) return true;
return false;
}
function isAndroidDevice() {
const ua = navigator.userAgent || '';
return /Android/i.test(ua) && !isIOSDevice();
}
const MODAL_WRAP =
'position:fixed;inset:0;z-index:10050;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;';
const MODAL_BACK = 'position:absolute;inset:0;background:rgba(0,0,0,.45);cursor:pointer;';
const MODAL_PANEL =
'position:relative;max-width:420px;width:100%;margin:auto;padding:20px 22px;border-radius:12px;background:#fff;color:#1a1a1a;font:15px/1.45 system-ui,-apple-system,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.18);';
const MODAL_TITLE = 'margin:0 0 10px;font-size:18px;font-weight:700;line-height:1.25;';
const MODAL_BODY = 'margin:0 0 18px;font-size:14px;color:#444;';
const MODAL_ACTIONS = 'display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end;';
const BTN =
'cursor:pointer;border-radius:8px;padding:10px 14px;font-size:14px;font-weight:600;border:1px solid #ccc;background:#f5f5f5;color:#1a1a1a;';
const BTN_PRIMARY = 'border-color:#1a6cff;background:#1a6cff;color:#fff;';
function initInAppBrowserModal() {
if (document.getElementById('inapp-browser-modal')) return;
const wrap = document.createElement('div');
wrap.id = 'inapp-browser-modal';
wrap.setAttribute('role', 'dialog');
wrap.setAttribute('aria-modal', 'true');
wrap.setAttribute('aria-hidden', 'true');
wrap.style.cssText = MODAL_WRAP + 'display:none;';
wrap.innerHTML =
'<div data-inapp-close style="' +
MODAL_BACK +
'"></div>' +
'<div style="' +
MODAL_PANEL +
'">' +
'<h3 id="inapp-modal-title" style="' +
MODAL_TITLE +
'"></h3>' +
'<p id="inapp-modal-body" style="' +
MODAL_BODY +
'"></p>' +
'<div style="' +
MODAL_ACTIONS +
'">' +
'<button type="button" id="inapp-copy-apk" style="' +
BTN +
BTN_PRIMARY +
'"></button>' +
'<button type="button" id="inapp-try-chrome" style="' +
BTN +
'" hidden></button>' +
'<button type="button" id="inapp-got-it" data-inapp-close style="' +
BTN +
'"></button>' +
'</div></div>';
document.body.appendChild(wrap);
const close = () => {
wrap.style.display = 'none';
wrap.setAttribute('aria-hidden', 'true');
};
wrap.querySelectorAll('[data-inapp-close]').forEach((el) => {
el.addEventListener('click', close);
});
const i18n = () => getI18n();
document.getElementById('inapp-copy-apk').addEventListener('click', async () => {
const btn = document.getElementById('inapp-copy-apk');
const dict = i18n();
const done = dict.inapp_browser_copied || 'Copied';
try {
await navigator.clipboard.writeText(getApkDownloadUrl());
const prev = btn.textContent;
btn.textContent = done;
setTimeout(() => {
btn.textContent = dict.inapp_browser_copy || prev;
}, 1600);
} catch {
window.prompt(dict.inapp_browser_copy || 'Copy', getApkDownloadUrl());
}
});
document.getElementById('inapp-try-chrome').addEventListener('click', () => {
const enc = encodeURIComponent(getApkDownloadUrl());
const intent =
getApkDownloadUrl().replace(/^https:/i, 'intent:') +
'#Intent;' +
'scheme=https;action=android.intent.action.VIEW;category=android.intent.category.BROWSABLE;' +
'package=com.android.chrome;S.browser_fallback_url=' +
enc +
';end';
window.location.href = intent;
});
}
function openInAppBrowserModal() {
const modal = document.getElementById('inapp-browser-modal');
if (!modal) return;
const dict = getI18n();
const tryChrome = modal.querySelector('#inapp-try-chrome');
const showChrome = isAndroidDevice() && !!detectInAppBrowser();
if (tryChrome) {
tryChrome.hidden = !showChrome;
tryChrome.setAttribute('aria-hidden', showChrome ? 'false' : 'true');
}
modal.style.display = 'flex';
modal.setAttribute('aria-hidden', 'false');
const titleEl = modal.querySelector('#inapp-modal-title');
const bodyEl = modal.querySelector('#inapp-modal-body');
if (titleEl) titleEl.textContent = dict.inapp_browser_title || '';
if (bodyEl) {
const iosWechat = isIOSDevice() && detectInAppBrowser() === 'wechat';
const bodyKey = iosWechat ? 'inapp_browser_body_ios' : 'inapp_browser_body';
bodyEl.innerHTML =
dict[bodyKey] || dict.inapp_browser_body || '';
}
const copyBtn = modal.querySelector('#inapp-copy-apk');
const gotBtn = modal.querySelector('#inapp-got-it');
if (copyBtn) copyBtn.textContent = dict.inapp_browser_copy || '';
if (tryChrome) tryChrome.textContent = dict.inapp_browser_try_chrome || '';
if (gotBtn) gotBtn.textContent = dict.inapp_browser_got_it || 'OK';
}
function initAppStoreSoonModal() {
if (document.getElementById('app-store-soon-modal')) return;
const wrap = document.createElement('div');
wrap.id = 'app-store-soon-modal';
wrap.setAttribute('role', 'dialog');
wrap.setAttribute('aria-modal', 'true');
wrap.setAttribute('aria-hidden', 'true');
wrap.style.cssText = MODAL_WRAP + 'display:none;';
wrap.innerHTML =
'<div data-app-soon-close style="' +
MODAL_BACK +
'"></div>' +
'<div style="' +
MODAL_PANEL +
'">' +
'<h3 id="app-soon-title" style="' +
MODAL_TITLE +
'"></h3>' +
'<p id="app-soon-body" style="' +
MODAL_BODY +
'"></p>' +
'<div style="' +
MODAL_ACTIONS +
'">' +
'<button type="button" data-app-soon-close style="' +
BTN +
BTN_PRIMARY +
'"></button>' +
'</div></div>';
document.body.appendChild(wrap);
const close = () => {
wrap.style.display = 'none';
wrap.setAttribute('aria-hidden', 'true');
};
wrap.querySelectorAll('[data-app-soon-close]').forEach((el) => {
el.addEventListener('click', close);
});
}
function openAppStoreSoonModal() {
const modal = document.getElementById('app-store-soon-modal');
if (!modal) return;
const dict = getI18n();
const tEl = modal.querySelector('#app-soon-title');
const bEl = modal.querySelector('#app-soon-body');
const ok = modal.querySelector('button[data-app-soon-close]');
if (tEl) tEl.textContent = dict.app_store_soon_title || '';
if (bEl) bEl.innerHTML = dict.app_store_soon_body || '';
if (ok) ok.textContent = dict.app_store_soon_ok || 'OK';
modal.style.display = 'flex';
modal.setAttribute('aria-hidden', 'false');
}
function bindBrowserDownloadLinks() {
document.querySelectorAll('a.store-badge--browser').forEach((a) => {
const openForInApp = (e) => {
if (!detectInAppBrowser()) return;
e.preventDefault();
e.stopPropagation();
openInAppBrowserModal();
};
a.addEventListener('click', openForInApp);
a.addEventListener(
'touchend',
(e) => {
if (!detectInAppBrowser()) return;
e.preventDefault();
openInAppBrowserModal();
},
{ passive: false }
);
});
}
function bindAppStoreAppleBadges() {
document.querySelectorAll('a.store-badge--apple[data-app-soon]').forEach((a) => {
const stopAndOpen = (e) => {
e.preventDefault();
e.stopPropagation();
openAppStoreSoonModal();
};
a.addEventListener('click', stopAndOpen);
a.addEventListener(
'touchend',
(e) => {
e.preventDefault();
e.stopPropagation();
openAppStoreSoonModal();
},
{ passive: false }
);
});
}
function loadSiteLinksThenBind() {
if (window.location.protocol === 'file:') {
bindBrowserDownloadLinks();
bindAppStoreAppleBadges();
return;
}
fetch('/api/site-links', { credentials: 'same-origin' })
.then((r) => {
if (!r.ok) throw new Error('bad status');
return r.json();
})
.then((j) => {
const apk = String(j.apk_download_url || '').trim();
const ios = String(j.app_store_url || '').trim();
if (apk.startsWith('https://')) apkDownloadUrl = apk;
document.querySelectorAll('a.store-badge--browser').forEach((a) => {
if (apk.startsWith('https://')) a.setAttribute('href', apk);
});
document.querySelectorAll('a.store-badge--apple').forEach((a) => {
if (ios.startsWith('https://')) {
a.setAttribute('href', ios);
a.removeAttribute('data-app-soon');
a.setAttribute('rel', 'noopener noreferrer');
a.setAttribute('target', '_blank');
}
});
const meta = document.getElementById('site-links-meta');
if (meta) {
const dict = getI18n();
const u = String(j.updated_at || '').trim();
if (u) {
meta.textContent =
(dict.linksMetaPrefix || 'Store links file updated: ') +
u +
(dict.linksMetaSuffix || ' (GMT+8)');
meta.hidden = false;
} else {
meta.hidden = true;
}
}
})
.catch(() => {
/* keep defaults */
})
.finally(() => {
bindBrowserDownloadLinks();
bindAppStoreAppleBadges();
});
}
initInAppBrowserModal();
initAppStoreSoonModal();
loadSiteLinksThenBind();
})();

71
scripts/deploy-talkpro.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Build Astro dist/ and sync to the Talk Pro marketing host (talkpro.info).
# Used by Gitea Actions and for manual deploys from this repo.
#
# Required (CI): TALKPRO_SSH_PRIVATE_KEY — PEM contents (Gitea secret)
# Optional: TALKPRO_SSH_KEY_FILE — path to PEM (local)
# Defaults match .env.deploy.example / Gitea workflow job env:
# TALKPRO_HOST=13.214.179.69 TALKPRO_USER=ubuntu TALKPRO_REMOTE_ROOT=/home/ubuntu/talkpro
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
HOST="${TALKPRO_HOST:-13.214.179.69}"
USER="${TALKPRO_USER:-ubuntu}"
REMOTE_ROOT="${TALKPRO_REMOTE_ROOT:-/home/ubuntu/talkpro}"
[[ -n "$HOST" ]] || HOST=13.214.179.69
[[ -n "$USER" ]] || USER=ubuntu
[[ -n "$REMOTE_ROOT" ]] || REMOTE_ROOT=/home/ubuntu/talkpro
KEY_FILE="${TALKPRO_SSH_KEY_FILE:-}"
TMP_KEY=""
cleanup() {
[[ -n "$TMP_KEY" && -f "$TMP_KEY" ]] && rm -f "$TMP_KEY"
}
trap cleanup EXIT
if [[ -n "${TALKPRO_SSH_PRIVATE_KEY:-}" ]]; then
TMP_KEY="$(mktemp)"
chmod 600 "$TMP_KEY"
printf '%s\n' "$TALKPRO_SSH_PRIVATE_KEY" | tr -d '\r' > "$TMP_KEY"
KEY_FILE="$TMP_KEY"
elif [[ -z "$KEY_FILE" || ! -f "$KEY_FILE" ]]; then
echo "ERROR: Set TALKPRO_SSH_PRIVATE_KEY (Gitea secret / env) or TALKPRO_SSH_KEY_FILE (local path)" >&2
exit 1
fi
if [[ ! -s "$KEY_FILE" ]]; then
echo "ERROR: SSH key file is empty — check TALKPRO_SSH_PRIVATE_KEY secret in Gitea" >&2
exit 1
fi
chmod 600 "$KEY_FILE"
SSH_OPTS=(-i "$KEY_FILE" -o BatchMode=yes -o StrictHostKeyChecking=accept-new)
RSYNC_SSH="ssh -i \"$KEY_FILE\" -o BatchMode=yes -o StrictHostKeyChecking=accept-new"
REMOTE="${USER}@${HOST}"
if [[ -f package-lock.json ]]; then
npm ci
else
npm install
fi
npm run build
command -v ssh >/dev/null || { echo "ERROR: ssh not found (install openssh-client)" >&2; exit 127; }
ssh "${SSH_OPTS[@]}" "$REMOTE" "mkdir -p ${REMOTE_ROOT}"
if command -v rsync >/dev/null 2>&1; then
echo "Uploading with rsync..."
rsync -avz --delete \
-e "$RSYNC_SSH" \
dist/ \
"${REMOTE}:${REMOTE_ROOT}/"
else
command -v tar >/dev/null || { echo "ERROR: need rsync or tar on the runner" >&2; exit 127; }
echo "Uploading with tar over ssh (rsync not available on runner)..."
ssh "${SSH_OPTS[@]}" "$REMOTE" "mkdir -p ${REMOTE_ROOT} && rm -rf ${REMOTE_ROOT:?}/*"
tar -C dist -cf - . | ssh "${SSH_OPTS[@]}" "$REMOTE" "tar -C ${REMOTE_ROOT} -xf -"
fi
echo "Deployed dist/ → ${REMOTE}:${REMOTE_ROOT}/"

View File

@@ -3,14 +3,17 @@ import type { Translations } from '../i18n/translations'
export interface Props { export interface Props {
t: Translations['download'] t: Translations['download']
siteLinks: Translations['siteLinks']
} }
const { t } = Astro.props const { t, siteLinks } = Astro.props
const bgPattern = "/assets/cta-bg-pattern.svg"; const bgPattern = "/assets/cta-bg-pattern.svg";
const talkproLogo = "/assets/cta-talkpro-logo.svg"; const talkproLogo = "/assets/cta-talkpro-logo.svg";
const androidIcon = "/assets/cta-android-icon.svg"; const androidIcon = "/assets/cta-android-icon.svg";
const appleIcon = "/assets/cta-apple-icon.svg"; const appleIcon = "/assets/cta-apple-icon.svg";
const phoneArt = "/assets/cta-phone-art.png"; const phoneArt = "/assets/cta-phone-art.png";
const defaultApkHref = "https://talkspro.xyz/download";
const siteLinksJson = JSON.stringify(siteLinks);
--- ---
<section id="download" class="download-cta"> <section id="download" class="download-cta">
@@ -35,7 +38,12 @@ const phoneArt = "/assets/cta-phone-art.png";
</div> </div>
<div class="store-badges"> <div class="store-badges">
<div class="store-badge store-badge--android"> <a
class="store-badge store-badge--android store-badge--browser"
href={defaultApkHref}
rel="noopener noreferrer"
target="_blank"
>
<div class="store-badge__icon-frame"> <div class="store-badge__icon-frame">
<img alt={t.androidAlt} class="store-badge__android-icon" src={androidIcon} /> <img alt={t.androidAlt} class="store-badge__android-icon" src={androidIcon} />
</div> </div>
@@ -43,8 +51,12 @@ const phoneArt = "/assets/cta-phone-art.png";
<p class="store-badge__platform">{t.android}</p> <p class="store-badge__platform">{t.android}</p>
<p class="store-badge__label">{t.androidCta}</p> <p class="store-badge__label">{t.androidCta}</p>
</div> </div>
</div> </a>
<div class="store-badge store-badge--ios"> <a
class="store-badge store-badge--ios store-badge--apple"
href="#"
data-app-soon="1"
>
<div class="store-badge__icon-frame"> <div class="store-badge__icon-frame">
<img alt={t.appleAlt} class="store-badge__apple-icon" src={appleIcon} /> <img alt={t.appleAlt} class="store-badge__apple-icon" src={appleIcon} />
</div> </div>
@@ -52,8 +64,13 @@ const phoneArt = "/assets/cta-phone-art.png";
<p class="store-badge__platform">{t.ios}</p> <p class="store-badge__platform">{t.ios}</p>
<p class="store-badge__label">{t.iosCta}</p> <p class="store-badge__label">{t.iosCta}</p>
</div> </div>
</a>
</div> </div>
</div> <p
id="site-links-meta"
class="download-cta__links-meta"
hidden
/>
</div> </div>
<div class="download-cta__phone"> <div class="download-cta__phone">
@@ -63,3 +80,5 @@ const phoneArt = "/assets/cta-phone-art.png";
</div> </div>
</div> </div>
</section> </section>
<script type="application/json" id="site-links-i18n" set:html={siteLinksJson} />

View File

@@ -11,6 +11,7 @@ const heroBg = "/assets/hero-bg.png";
const phoneMockup = "/assets/hero-phone.png"; const phoneMockup = "/assets/hero-phone.png";
const androidIcon = "/assets/cta-android-icon.svg"; const androidIcon = "/assets/cta-android-icon.svg";
const appleIcon = "/assets/cta-apple-icon.svg"; const appleIcon = "/assets/cta-apple-icon.svg";
const defaultApkHref = "https://talkspro.xyz/download";
--- ---
<section id="hero" class="hero"> <section id="hero" class="hero">
@@ -39,7 +40,12 @@ const appleIcon = "/assets/cta-apple-icon.svg";
{t.description} {t.description}
</p> </p>
<div class="hero__actions"> <div class="hero__actions">
<a href="#download" class="store-badge store-badge--android hero__store-badge"> <a
href={defaultApkHref}
class="store-badge store-badge--android store-badge--browser hero__store-badge"
rel="noopener noreferrer"
target="_blank"
>
<div class="store-badge__icon-frame"> <div class="store-badge__icon-frame">
<img alt={download.androidAlt} class="store-badge__android-icon" src={androidIcon} /> <img alt={download.androidAlt} class="store-badge__android-icon" src={androidIcon} />
</div> </div>
@@ -48,7 +54,11 @@ const appleIcon = "/assets/cta-apple-icon.svg";
<p class="store-badge__label">{download.androidCta}</p> <p class="store-badge__label">{download.androidCta}</p>
</div> </div>
</a> </a>
<a href="#download" class="store-badge store-badge--ios hero__store-badge"> <a
href="#"
class="store-badge store-badge--ios store-badge--apple hero__store-badge"
data-app-soon="1"
>
<div class="store-badge__icon-frame"> <div class="store-badge__icon-frame">
<img alt={download.appleAlt} class="store-badge__apple-icon" src={appleIcon} /> <img alt={download.appleAlt} class="store-badge__apple-icon" src={appleIcon} />
</div> </div>

View File

@@ -274,13 +274,30 @@ export const translations = {
description: description:
"Download TalkPro and experience a cleaner, simpler, and more modern way to stay connected.", "Download TalkPro and experience a cleaner, simpler, and more modern way to stay connected.",
android: "ANDROID", android: "ANDROID",
androidCta: "APK Coming Soon", androidCta: "APK Download",
ios: "IOS", ios: "IOS",
iosCta: "Get it on the App Store", iosCta: "Get it on the App Store",
androidAlt: "Android", androidAlt: "Android",
appleAlt: "Apple", appleAlt: "Apple",
phoneAlt: "TalkPro on phone", phoneAlt: "TalkPro on phone",
}, },
siteLinks: {
linksMetaPrefix: "Store links file updated: ",
linksMetaSuffix: " (GMT+8)",
inapp_browser_title: "Open in your system browser",
inapp_browser_body:
"This page is opened inside an <strong>in-app browser</strong>. To download the APK, tap the <strong>menu (··· or ⋮)</strong> in the upper corner, then choose <strong>Open in default browser</strong>, <strong>Open in Safari</strong>, or <strong>Open in Chrome</strong>. Return here and tap the Android badge again—or paste the link you copied.",
inapp_browser_body_ios:
"Youre in <strong>WeChats built-in browser</strong> on iPhone or iPad. Apple and WeChat <strong>do not allow</strong> a website to jump to Safari automatically—this is normal. Tap <strong>···</strong> (top right) → <strong>Open in Safari</strong> or <strong>Open in default browser</strong>, then tap the Android badge again, or use <strong>Copy download link</strong> and paste it in Safari.",
inapp_browser_copy: "Copy download link",
inapp_browser_copied: "Copied",
inapp_browser_got_it: "OK",
inapp_browser_try_chrome: "Try opening in Chrome (Android)",
app_store_soon_title: "Coming soon",
app_store_soon_body:
"The <strong>App Store</strong> download is still in development and is <strong>not available</strong> yet. Please use the <strong>Android</strong> download for now.",
app_store_soon_ok: "OK",
},
footer: { footer: {
logoAlt: "TalkPro", logoAlt: "TalkPro",
description: description:
@@ -294,7 +311,7 @@ export const translations = {
email: "email@hotmail.com", email: "email@hotmail.com",
phone: "+01 123 45562334", phone: "+01 123 45562334",
android: "ANDROID", android: "ANDROID",
androidCta: "APK Coming Soon", androidCta: "APK Download",
ios: "IOS", ios: "IOS",
iosCta: "Get it on the App Store", iosCta: "Get it on the App Store",
copyright: "© 2026 TalkPro. All rights reserved.", copyright: "© 2026 TalkPro. All rights reserved.",
@@ -335,7 +352,7 @@ export const translations = {
description: description:
"Descarga TalkPro y vive una forma más limpia, simple y moderna de mantenerte conectado.", "Descarga TalkPro y vive una forma más limpia, simple y moderna de mantenerte conectado.",
android: "ANDROID", android: "ANDROID",
androidCta: "APK próximamente", androidCta: "Descargar APK",
ios: "IOS", ios: "IOS",
iosCta: "Consíguelo en App Store", iosCta: "Consíguelo en App Store",
}, },
@@ -381,7 +398,7 @@ export const translations = {
description: description:
"Tải TalkPro và trải nghiệm cách kết nối sạch hơn, đơn giản hơn và hiện đại hơn.", "Tải TalkPro và trải nghiệm cách kết nối sạch hơn, đơn giản hơn và hiện đại hơn.",
android: "ANDROID", android: "ANDROID",
androidCta: "APK sắp ra mắt", androidCta: "Tải APK",
ios: "IOS", ios: "IOS",
iosCta: "Tải trên App Store", iosCta: "Tải trên App Store",
}, },
@@ -529,13 +546,30 @@ export const translations = {
logoAlt: "TalkPro", logoAlt: "TalkPro",
description: "下载 TalkPro体验更清晰、更简单、更现代的连接方式。", description: "下载 TalkPro体验更清晰、更简单、更现代的连接方式。",
android: "安卓", android: "安卓",
androidCta: "APK 即将推出", androidCta: "APK 下载",
ios: "IOS", ios: "IOS",
iosCta: "在 App Store 上获取", iosCta: "在 App Store 上获取",
androidAlt: "Android", androidAlt: "Android",
appleAlt: "Apple", appleAlt: "Apple",
phoneAlt: "手机上的 TalkPro", phoneAlt: "手机上的 TalkPro",
}, },
siteLinks: {
linksMetaPrefix: "Store links file updated: ",
linksMetaSuffix: " (GMT+8)",
inapp_browser_title: "请在系统浏览器中打开",
inapp_browser_body:
"当前页面在<strong>应用内置浏览器</strong>中打开,无法可靠下载 APK。请点击右上角<strong>「···」或「⋮」</strong>菜单,选择<strong>「在浏览器打开」「用默认浏览器打开」「在 Safari 打开」</strong>或<strong>「在 Chrome 打开」</strong>,在系统浏览器中再次点击 Android 徽章;或使用下方按钮<strong>复制链接</strong>,粘贴到系统浏览器地址栏打开。",
inapp_browser_body_ios:
"您正在 iPhone/iPad 的<strong>微信内置浏览器</strong>中。微信与 iOS <strong>不允许</strong>网页自动跳转到系统 Safari这是系统限制不是网站故障。请点击右上角<strong>「···」</strong>,选择<strong>「在 Safari 中打开」</strong>或<strong>「在默认浏览器中打开」</strong>,再在 Safari 里点 Android 徽章;或先<strong>复制下载链接</strong>,到 Safari 地址栏粘贴打开。",
inapp_browser_copy: "复制下载链接",
inapp_browser_copied: "已复制",
inapp_browser_got_it: "知道了",
inapp_browser_try_chrome: "尝试用 Chrome 打开Android",
app_store_soon_title: "开发中",
app_store_soon_body:
"<strong>App Store</strong> 下载尚在开发与上架准备中,<strong>暂时未开放</strong>。请先使用 <strong>Android</strong> 下载。带来不便敬请谅解。",
app_store_soon_ok: "知道了",
},
footer: { footer: {
logoAlt: "TalkPro", logoAlt: "TalkPro",
description: description:
@@ -549,7 +583,7 @@ export const translations = {
email: "email@hotmail.com", email: "email@hotmail.com",
phone: "+01 123 45562334", phone: "+01 123 45562334",
android: "安卓", android: "安卓",
androidCta: "APK 即将推出", androidCta: "APK 下载",
ios: "IOS", ios: "IOS",
iosCta: "在 App Store 上获取", iosCta: "在 App Store 上获取",
copyright: "© 2026 TalkPro. 版权所有。", copyright: "© 2026 TalkPro. 版权所有。",
@@ -692,13 +726,30 @@ export const translations = {
logoAlt: "TalkPro", logoAlt: "TalkPro",
description: "下載 TalkPro體驗更清晰、更簡單、更現代的連結方式。", description: "下載 TalkPro體驗更清晰、更簡單、更現代的連結方式。",
android: "安卓", android: "安卓",
androidCta: "APK 即將推出", androidCta: "APK 下載",
ios: "IOS", ios: "IOS",
iosCta: "在 App Store 上取得", iosCta: "在 App Store 上取得",
androidAlt: "Android", androidAlt: "Android",
appleAlt: "Apple", appleAlt: "Apple",
phoneAlt: "手機上的 TalkPro", phoneAlt: "手機上的 TalkPro",
}, },
siteLinks: {
linksMetaPrefix: "Store links file updated: ",
linksMetaSuffix: " (GMT+8)",
inapp_browser_title: "請在系統瀏覽器中開啟",
inapp_browser_body:
"目前頁面在<strong>應用程式內建瀏覽器</strong>中開啟,無法可靠下載 APK。請點選右上角<strong>「···」或「⋮」</strong>選單,選擇<strong>「在瀏覽器開啟」「用預設瀏覽器開啟」「在 Safari 開啟」</strong>或<strong>「在 Chrome 開啟」</strong>,在系統瀏覽器中再次點選 Android 徽章;或使用下方按鈕<strong>複製連結</strong>,貼到系統瀏覽器網址列開啟。",
inapp_browser_body_ios:
"您正在 iPhone/iPad 的<strong>微信內建瀏覽器</strong>中。微信與 iOS <strong>不允許</strong>網頁自動跳轉到系統 Safari這是系統限制不是網站故障。請點選右上角<strong>「···」</strong>,選擇<strong>「在 Safari 中開啟」</strong>或<strong>「在預設瀏覽器中開啟」</strong>,再在 Safari 裡點 Android 徽章;或先<strong>複製下載連結</strong>,到 Safari 網址列貼上開啟。",
inapp_browser_copy: "複製下載連結",
inapp_browser_copied: "已複製",
inapp_browser_got_it: "知道了",
inapp_browser_try_chrome: "嘗試用 Chrome 開啟Android",
app_store_soon_title: "開發中",
app_store_soon_body:
"<strong>App Store</strong> 下載尚在開發與上架準備中,<strong>暫時未開放</strong>。請先使用 <strong>Android</strong> 下載。造成不便敬請見諒。",
app_store_soon_ok: "知道了",
},
footer: { footer: {
logoAlt: "TalkPro", logoAlt: "TalkPro",
description: description:
@@ -712,7 +763,7 @@ export const translations = {
email: "email@hotmail.com", email: "email@hotmail.com",
phone: "+01 123 45562334", phone: "+01 123 45562334",
android: "安卓", android: "安卓",
androidCta: "APK 即將推出", androidCta: "APK 下載",
ios: "IOS", ios: "IOS",
iosCta: "在 App Store 上取得", iosCta: "在 App Store 上取得",
copyright: "© 2026 TalkPro. 版權所有。", copyright: "© 2026 TalkPro. 版權所有。",
@@ -895,7 +946,7 @@ export const translations = {
description: description:
"Muat turun TalkPro dan alami cara yang lebih bersih, ringkas dan moden untuk kekal berhubung.", "Muat turun TalkPro dan alami cara yang lebih bersih, ringkas dan moden untuk kekal berhubung.",
android: "ANDROID", android: "ANDROID",
androidCta: "APK Akan Datang", androidCta: "Muat Turun APK",
ios: "IOS", ios: "IOS",
iosCta: "Dapatkan di App Store", iosCta: "Dapatkan di App Store",
androidAlt: "Android", androidAlt: "Android",
@@ -915,7 +966,7 @@ export const translations = {
email: "email@hotmail.com", email: "email@hotmail.com",
phone: "+01 123 45562334", phone: "+01 123 45562334",
android: "ANDROID", android: "ANDROID",
androidCta: "APK Akan Datang", androidCta: "Muat Turun APK",
ios: "IOS", ios: "IOS",
iosCta: "Dapatkan di App Store", iosCta: "Dapatkan di App Store",
copyright: "© 2026 TalkPro. Hak cipta terpelihara.", copyright: "© 2026 TalkPro. Hak cipta terpelihara.",
@@ -1098,7 +1149,7 @@ export const translations = {
description: description:
"TalkPro をダウンロードして、よりクリーンでシンプル、モダンなつながり方を体験してください。", "TalkPro をダウンロードして、よりクリーンでシンプル、モダンなつながり方を体験してください。",
android: "ANDROID", android: "ANDROID",
androidCta: "APK 近日公開", androidCta: "APK ダウンロード",
ios: "IOS", ios: "IOS",
iosCta: "App Store で入手", iosCta: "App Store で入手",
androidAlt: "Android", androidAlt: "Android",
@@ -1118,7 +1169,7 @@ export const translations = {
email: "email@hotmail.com", email: "email@hotmail.com",
phone: "+01 123 45562334", phone: "+01 123 45562334",
android: "ANDROID", android: "ANDROID",
androidCta: "APK 近日公開", androidCta: "APK ダウンロード",
ios: "IOS", ios: "IOS",
iosCta: "App Store で入手", iosCta: "App Store で入手",
copyright: "© 2026 TalkPro. All rights reserved.", copyright: "© 2026 TalkPro. All rights reserved.",
@@ -1301,7 +1352,7 @@ export const translations = {
description: description:
"TalkPro를 다운로드하고 더 깔끔하고 단순하며 현대적인 연결 방식을 경험하세요.", "TalkPro를 다운로드하고 더 깔끔하고 단순하며 현대적인 연결 방식을 경험하세요.",
android: "안드로이드", android: "안드로이드",
androidCta: "APK 곧 출시", androidCta: "APK 다운로드",
ios: "IOS", ios: "IOS",
iosCta: "App Store에서 받기", iosCta: "App Store에서 받기",
androidAlt: "Android", androidAlt: "Android",
@@ -1321,7 +1372,7 @@ export const translations = {
email: "email@hotmail.com", email: "email@hotmail.com",
phone: "+01 123 45562334", phone: "+01 123 45562334",
android: "안드로이드", android: "안드로이드",
androidCta: "APK 곧 출시", androidCta: "APK 다운로드",
ios: "IOS", ios: "IOS",
iosCta: "App Store에서 받기", iosCta: "App Store에서 받기",
copyright: "© 2026 TalkPro. All rights reserved.", copyright: "© 2026 TalkPro. All rights reserved.",
@@ -1356,7 +1407,7 @@ export const translations = {
description: description:
"Baixe o TalkPro e experimente uma forma mais limpa, simples e moderna de se conectar.", "Baixe o TalkPro e experimente uma forma mais limpa, simples e moderna de se conectar.",
android: "ANDROID", android: "ANDROID",
androidCta: "APK em breve", androidCta: "Baixar APK",
ios: "IOS", ios: "IOS",
iosCta: "Baixar na App Store", iosCta: "Baixar na App Store",
}, },
@@ -1396,7 +1447,7 @@ export const translations = {
description: description:
"Lade TalkPro herunter und erlebe eine klarere, einfachere und modernere Art, verbunden zu bleiben.", "Lade TalkPro herunter und erlebe eine klarere, einfachere und modernere Art, verbunden zu bleiben.",
android: "ANDROID", android: "ANDROID",
androidCta: "APK bald verfügbar", androidCta: "APK herunterladen",
ios: "IOS", ios: "IOS",
iosCta: "Im App Store laden", iosCta: "Im App Store laden",
}, },
@@ -1436,7 +1487,7 @@ export const translations = {
description: description:
"Téléchargez TalkPro et découvrez une façon plus claire, simple et moderne de rester connecté.", "Téléchargez TalkPro et découvrez une façon plus claire, simple et moderne de rester connecté.",
android: "ANDROID", android: "ANDROID",
androidCta: "APK bientôt disponible", androidCta: "Télécharger l'APK",
ios: "IOS", ios: "IOS",
iosCta: "Disponible sur lApp Store", iosCta: "Disponible sur lApp Store",
}, },
@@ -1476,7 +1527,7 @@ export const translations = {
description: description:
"TalkPro डाउनलोड करें और जुड़े रहने का साफ, सरल और आधुनिक तरीका अनुभव करें।", "TalkPro डाउनलोड करें और जुड़े रहने का साफ, सरल और आधुनिक तरीका अनुभव करें।",
android: "एंड्रॉयड", android: "एंड्रॉयड",
androidCta: "APK जल्द आ रहा है", androidCta: "APK डाउनलोड",
ios: "IOS", ios: "IOS",
iosCta: "App Store से पाएं", iosCta: "App Store से पाएं",
}, },
@@ -1516,7 +1567,7 @@ export const translations = {
description: description:
"نزّل TalkPro واختبر طريقة أوضح وأبسط وأكثر حداثة للبقاء على اتصال.", "نزّل TalkPro واختبر طريقة أوضح وأبسط وأكثر حداثة للبقاء على اتصال.",
android: "أندرويد", android: "أندرويد",
androidCta: "APK قريبًا", androidCta: "تنزيل APK",
ios: "آي أو إس", ios: "آي أو إس",
iosCta: "احصل عليه من App Store", iosCta: "احصل عليه من App Store",
}, },
@@ -1556,7 +1607,7 @@ export const translations = {
description: description:
"Скачайте TalkPro и попробуйте более чистый, простой и современный способ общения.", "Скачайте TalkPro и попробуйте более чистый, простой и современный способ общения.",
android: "Андроид", android: "Андроид",
androidCta: "APK скоро", androidCta: "Скачать APK",
ios: "IOS", ios: "IOS",
iosCta: "Загрузить в App Store", iosCta: "Загрузить в App Store",
}, },
@@ -1596,7 +1647,7 @@ export const translations = {
description: description:
"Unduh TalkPro dan rasakan cara yang lebih bersih, sederhana, dan modern untuk tetap terhubung.", "Unduh TalkPro dan rasakan cara yang lebih bersih, sederhana, dan modern untuk tetap terhubung.",
android: "ANDROID", android: "ANDROID",
androidCta: "APK segera hadir", androidCta: "Unduh APK",
ios: "IOS", ios: "IOS",
iosCta: "Dapatkan di App Store", iosCta: "Dapatkan di App Store",
}, },
@@ -1636,7 +1687,7 @@ export const translations = {
description: description:
"TalkPro ڈاؤن لوڈ کریں اور جڑے رہنے کا صاف، سادہ اور جدید طریقہ آزمائیں۔", "TalkPro ڈاؤن لوڈ کریں اور جڑے رہنے کا صاف، سادہ اور جدید طریقہ آزمائیں۔",
android: "اینڈرائیڈ", android: "اینڈرائیڈ",
androidCta: "APK جلد آ رہا ہے", androidCta: "APK ڈاؤن لوڈ",
ios: "آئی او ایس", ios: "آئی او ایس",
iosCta: "App Store سے حاصل کریں", iosCta: "App Store سے حاصل کریں",
}, },

View File

@@ -27,6 +27,7 @@ const {
</head> </head>
<body class="bg-surface font-sans overflow-x-hidden"> <body class="bg-surface font-sans overflow-x-hidden">
<slot /> <slot />
<script src="/site-links-client.js" defer></script>
<script> <script>
(() => { (() => {
const header = document.getElementById('site-header'); const header = document.getElementById('site-header');

View File

@@ -31,6 +31,6 @@ const t = getTranslations(lang)
<UseCases t={t.useCases} /> <UseCases t={t.useCases} />
<Trust t={t.trust} /> <Trust t={t.trust} />
<!-- AppPreview section disabled per lead request. --> <!-- AppPreview section disabled per lead request. -->
<DownloadCTA t={t.download} /> <DownloadCTA t={t.download} siteLinks={t.siteLinks} />
<Footer t={t.footer} /> <Footer t={t.footer} />
</Base> </Base>

View File

@@ -24,6 +24,6 @@ const t = getTranslations(lang)
<UseCases t={t.useCases} /> <UseCases t={t.useCases} />
<Trust t={t.trust} /> <Trust t={t.trust} />
<!-- AppPreview section disabled per lead request. --> <!-- AppPreview section disabled per lead request. -->
<DownloadCTA t={t.download} /> <DownloadCTA t={t.download} siteLinks={t.siteLinks} />
<Footer t={t.footer} /> <Footer t={t.footer} />
</Base> </Base>

View File

@@ -129,6 +129,21 @@
border-radius: 20px; border-radius: 20px;
} }
a.store-badge {
box-sizing: border-box;
color: inherit;
text-decoration: none;
cursor: pointer;
}
.download-cta__links-meta {
font-size: 13px;
color: #7a726d;
margin: 14px auto 0;
max-width: min(1180px, calc(100% - 40px));
text-align: center;
}
.store-badge--android { .store-badge--android {
background: #f28a4b; background: #f28a4b;
border: 1px solid #c5834e; border: 1px solid #c5834e;