Compare commits
2 Commits
df6cff4895
...
6e90a4adb6
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e90a4adb6 | |||
| 3933cf42c0 |
7
.env.deploy.example
Normal file
7
.env.deploy.example
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Copy to .env.deploy for local runs: set -a && source .env.deploy && set +a && ./scripts/deploy-talkpro.sh
|
||||||
|
# In Gitea, use repository secrets instead (see .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
|
||||||
50
.gitea/workflows/deploy-talkpro.yml
Normal file
50
.gitea/workflows/deploy-talkpro.yml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Build talk-pro and rsync dist/ to the marketing VPS (SSH host talkpro).
|
||||||
|
#
|
||||||
|
# Gitea repo secrets (Settings → Secrets):
|
||||||
|
# TALKPRO_SSH_PRIVATE_KEY full PEM for ubuntu@talkpro (same as luis-only.pem)
|
||||||
|
# TALKPRO_HOST e.g. 13.214.179.69
|
||||||
|
#
|
||||||
|
# Optional secrets:
|
||||||
|
# TALKPRO_USER default ubuntu
|
||||||
|
# TALKPRO_REMOTE_ROOT default /home/ubuntu/talkpro
|
||||||
|
#
|
||||||
|
# Requires a runner with: node 22+, npm, rsync, ssh, ssh-keyscan.
|
||||||
|
# Site-links API (/api/site-links) is deployed separately from the parent talkpro repo.
|
||||||
|
|
||||||
|
name: Deploy to talkpro
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
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: Trust host key
|
||||||
|
env:
|
||||||
|
TALKPRO_HOST: ${{ secrets.TALKPRO_HOST }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
chmod 700 ~/.ssh
|
||||||
|
ssh-keyscan -H "$TALKPRO_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: Build and rsync to talkpro
|
||||||
|
env:
|
||||||
|
TALKPRO_HOST: ${{ secrets.TALKPRO_HOST }}
|
||||||
|
TALKPRO_USER: ${{ secrets.TALKPRO_USER }}
|
||||||
|
TALKPRO_REMOTE_ROOT: ${{ secrets.TALKPRO_REMOTE_ROOT }}
|
||||||
|
TALKPRO_SSH_PRIVATE_KEY: ${{ secrets.TALKPRO_SSH_PRIVATE_KEY }}
|
||||||
|
run: bash scripts/deploy-talkpro.sh
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,3 +5,5 @@ dist/
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
!.env.deploy.example
|
||||||
|
.env.deploy
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -26,6 +26,29 @@ 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 secrets** (Gitea → Settings → Secrets):
|
||||||
|
|
||||||
|
| Secret | Value |
|
||||||
|
|--------|--------|
|
||||||
|
| `TALKPRO_SSH_PRIVATE_KEY` | SSH private key (PEM) for `ubuntu@talkpro` |
|
||||||
|
| `TALKPRO_HOST` | Server IP, e.g. `13.214.179.69` |
|
||||||
|
| `TALKPRO_USER` | Optional; default `ubuntu` |
|
||||||
|
| `TALKPRO_REMOTE_ROOT` | Optional; default `/home/ubuntu/talkpro` |
|
||||||
|
|
||||||
|
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
310
public/site-links-client.js
Normal 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();
|
||||||
|
})();
|
||||||
58
scripts/deploy-talkpro.sh
Executable file
58
scripts/deploy-talkpro.sh
Executable file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build Astro dist/ and rsync to the Talk Pro marketing host (talkpro.info).
|
||||||
|
# Used by Gitea Actions and for manual deploys from this repo.
|
||||||
|
#
|
||||||
|
# Required env:
|
||||||
|
# TALKPRO_HOST e.g. 13.214.179.69
|
||||||
|
# Optional:
|
||||||
|
# TALKPRO_USER default ubuntu
|
||||||
|
# TALKPRO_REMOTE_ROOT default /home/ubuntu/talkpro
|
||||||
|
# TALKPRO_SSH_PRIVATE_KEY PEM contents (CI / Gitea secret)
|
||||||
|
# TALKPRO_SSH_KEY_FILE path to PEM (local)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
HOST="${TALKPRO_HOST:?Set TALKPRO_HOST (server IP or hostname)}"
|
||||||
|
USER="${TALKPRO_USER:-ubuntu}"
|
||||||
|
REMOTE_ROOT="${TALKPRO_REMOTE_ROOT:-/home/ubuntu/talkpro}"
|
||||||
|
[[ -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" > "$TMP_KEY"
|
||||||
|
KEY_FILE="$TMP_KEY"
|
||||||
|
elif [[ -z "$KEY_FILE" || ! -f "$KEY_FILE" ]]; then
|
||||||
|
echo "Set TALKPRO_SSH_PRIVATE_KEY (PEM) or TALKPRO_SSH_KEY_FILE (path)" >&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"
|
||||||
|
|
||||||
|
if [[ -f package-lock.json ]]; then
|
||||||
|
npm ci
|
||||||
|
else
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
"${SSH[@]}" "${USER}@${HOST}" "mkdir -p ${REMOTE_ROOT}"
|
||||||
|
|
||||||
|
rsync -avz --delete \
|
||||||
|
-e "$RSYNC_SSH" \
|
||||||
|
dist/ \
|
||||||
|
"${USER}@${HOST}:${REMOTE_ROOT}/"
|
||||||
|
|
||||||
|
echo "Deployed dist/ → ${USER}@${HOST}:${REMOTE_ROOT}/"
|
||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -281,6 +281,23 @@ export const translations = {
|
|||||||
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:
|
||||||
|
"You’re in <strong>WeChat’s 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:
|
||||||
@@ -536,6 +553,23 @@ export const translations = {
|
|||||||
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:
|
||||||
@@ -699,6 +733,23 @@ export const translations = {
|
|||||||
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:
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user