Compare commits
12 Commits
7b45ca94a6
...
b8103ea072
| Author | SHA1 | Date | |
|---|---|---|---|
| b8103ea072 | |||
| a481c382a6 | |||
| 5f9e8db7e5 | |||
| fe14ca30ff | |||
| cf9cbb6134 | |||
| 77485eee63 | |||
| 6e90a4adb6 | |||
| 3933cf42c0 | |||
| df6cff4895 | |||
| 82e3a23df3 | |||
| 66f52a2d6e | |||
| b6e6178466 |
10
.env.deploy.example
Normal file
10
.env.deploy.example
Normal 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
|
||||
57
.gitea/workflows/deploy-talkpro.yml
Normal file
57
.gitea/workflows/deploy-talkpro.yml
Normal 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
2
.gitignore
vendored
@@ -5,3 +5,5 @@ dist/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.deploy.example
|
||||
.env.deploy
|
||||
|
||||
22
README.md
22
README.md
@@ -26,6 +26,28 @@ Open [http://localhost:4321](http://localhost:4321)
|
||||
| `npm run build` | Build for production into `dist/` |
|
||||
| `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
|
||||
|
||||
```
|
||||
|
||||
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();
|
||||
})();
|
||||
71
scripts/deploy-talkpro.sh
Executable file
71
scripts/deploy-talkpro.sh
Executable 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}/"
|
||||
@@ -3,14 +3,17 @@ import type { Translations } from '../i18n/translations'
|
||||
|
||||
export interface Props {
|
||||
t: Translations['download']
|
||||
siteLinks: Translations['siteLinks']
|
||||
}
|
||||
|
||||
const { t } = Astro.props
|
||||
const { t, siteLinks } = Astro.props
|
||||
const bgPattern = "/assets/cta-bg-pattern.svg";
|
||||
const talkproLogo = "/assets/cta-talkpro-logo.svg";
|
||||
const androidIcon = "/assets/cta-android-icon.svg";
|
||||
const appleIcon = "/assets/cta-apple-icon.svg";
|
||||
const phoneArt = "/assets/cta-phone-art.png";
|
||||
const defaultApkHref = "https://talkspro.xyz/download";
|
||||
const siteLinksJson = JSON.stringify(siteLinks);
|
||||
---
|
||||
|
||||
<section id="download" class="download-cta">
|
||||
@@ -35,7 +38,12 @@ const phoneArt = "/assets/cta-phone-art.png";
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<img alt={t.androidAlt} class="store-badge__android-icon" src={androidIcon} />
|
||||
</div>
|
||||
@@ -43,8 +51,12 @@ const phoneArt = "/assets/cta-phone-art.png";
|
||||
<p class="store-badge__platform">{t.android}</p>
|
||||
<p class="store-badge__label">{t.androidCta}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="store-badge store-badge--ios">
|
||||
</a>
|
||||
<a
|
||||
class="store-badge store-badge--ios store-badge--apple"
|
||||
href="#"
|
||||
data-app-soon="1"
|
||||
>
|
||||
<div class="store-badge__icon-frame">
|
||||
<img alt={t.appleAlt} class="store-badge__apple-icon" src={appleIcon} />
|
||||
</div>
|
||||
@@ -52,8 +64,13 @@ const phoneArt = "/assets/cta-phone-art.png";
|
||||
<p class="store-badge__platform">{t.ios}</p>
|
||||
<p class="store-badge__label">{t.iosCta}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
id="site-links-meta"
|
||||
class="download-cta__links-meta"
|
||||
hidden
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="download-cta__phone">
|
||||
@@ -63,3 +80,5 @@ const phoneArt = "/assets/cta-phone-art.png";
|
||||
</div>
|
||||
</div>
|
||||
</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 androidIcon = "/assets/cta-android-icon.svg";
|
||||
const appleIcon = "/assets/cta-apple-icon.svg";
|
||||
const defaultApkHref = "https://talkspro.xyz/download";
|
||||
---
|
||||
|
||||
<section id="hero" class="hero">
|
||||
@@ -39,7 +40,12 @@ const appleIcon = "/assets/cta-apple-icon.svg";
|
||||
{t.description}
|
||||
</p>
|
||||
<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">
|
||||
<img alt={download.androidAlt} class="store-badge__android-icon" src={androidIcon} />
|
||||
</div>
|
||||
@@ -48,7 +54,11 @@ const appleIcon = "/assets/cta-apple-icon.svg";
|
||||
<p class="store-badge__label">{download.androidCta}</p>
|
||||
</div>
|
||||
</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">
|
||||
<img alt={download.appleAlt} class="store-badge__apple-icon" src={appleIcon} />
|
||||
</div>
|
||||
|
||||
@@ -274,13 +274,30 @@ export const translations = {
|
||||
description:
|
||||
"Download TalkPro and experience a cleaner, simpler, and more modern way to stay connected.",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK Coming Soon",
|
||||
androidCta: "APK Download",
|
||||
ios: "IOS",
|
||||
iosCta: "Get it on the App Store",
|
||||
androidAlt: "Android",
|
||||
appleAlt: "Apple",
|
||||
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: {
|
||||
logoAlt: "TalkPro",
|
||||
description:
|
||||
@@ -294,7 +311,7 @@ export const translations = {
|
||||
email: "email@hotmail.com",
|
||||
phone: "+01 123 45562334",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK Coming Soon",
|
||||
androidCta: "APK Download",
|
||||
ios: "IOS",
|
||||
iosCta: "Get it on the App Store",
|
||||
copyright: "© 2026 TalkPro. All rights reserved.",
|
||||
@@ -335,7 +352,7 @@ export const translations = {
|
||||
description:
|
||||
"Descarga TalkPro y vive una forma más limpia, simple y moderna de mantenerte conectado.",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK próximamente",
|
||||
androidCta: "Descargar APK",
|
||||
ios: "IOS",
|
||||
iosCta: "Consíguelo en App Store",
|
||||
},
|
||||
@@ -381,7 +398,7 @@ export const translations = {
|
||||
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.",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK sắp ra mắt",
|
||||
androidCta: "Tải APK",
|
||||
ios: "IOS",
|
||||
iosCta: "Tải trên App Store",
|
||||
},
|
||||
@@ -529,13 +546,30 @@ export const translations = {
|
||||
logoAlt: "TalkPro",
|
||||
description: "下载 TalkPro,体验更清晰、更简单、更现代的连接方式。",
|
||||
android: "安卓",
|
||||
androidCta: "APK 即将推出",
|
||||
androidCta: "APK 下载",
|
||||
ios: "IOS",
|
||||
iosCta: "在 App Store 上获取",
|
||||
androidAlt: "Android",
|
||||
appleAlt: "Apple",
|
||||
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: {
|
||||
logoAlt: "TalkPro",
|
||||
description:
|
||||
@@ -549,7 +583,7 @@ export const translations = {
|
||||
email: "email@hotmail.com",
|
||||
phone: "+01 123 45562334",
|
||||
android: "安卓",
|
||||
androidCta: "APK 即将推出",
|
||||
androidCta: "APK 下载",
|
||||
ios: "IOS",
|
||||
iosCta: "在 App Store 上获取",
|
||||
copyright: "© 2026 TalkPro. 版权所有。",
|
||||
@@ -692,13 +726,30 @@ export const translations = {
|
||||
logoAlt: "TalkPro",
|
||||
description: "下載 TalkPro,體驗更清晰、更簡單、更現代的連結方式。",
|
||||
android: "安卓",
|
||||
androidCta: "APK 即將推出",
|
||||
androidCta: "APK 下載",
|
||||
ios: "IOS",
|
||||
iosCta: "在 App Store 上取得",
|
||||
androidAlt: "Android",
|
||||
appleAlt: "Apple",
|
||||
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: {
|
||||
logoAlt: "TalkPro",
|
||||
description:
|
||||
@@ -712,7 +763,7 @@ export const translations = {
|
||||
email: "email@hotmail.com",
|
||||
phone: "+01 123 45562334",
|
||||
android: "安卓",
|
||||
androidCta: "APK 即將推出",
|
||||
androidCta: "APK 下載",
|
||||
ios: "IOS",
|
||||
iosCta: "在 App Store 上取得",
|
||||
copyright: "© 2026 TalkPro. 版權所有。",
|
||||
@@ -895,7 +946,7 @@ export const translations = {
|
||||
description:
|
||||
"Muat turun TalkPro dan alami cara yang lebih bersih, ringkas dan moden untuk kekal berhubung.",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK Akan Datang",
|
||||
androidCta: "Muat Turun APK",
|
||||
ios: "IOS",
|
||||
iosCta: "Dapatkan di App Store",
|
||||
androidAlt: "Android",
|
||||
@@ -915,7 +966,7 @@ export const translations = {
|
||||
email: "email@hotmail.com",
|
||||
phone: "+01 123 45562334",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK Akan Datang",
|
||||
androidCta: "Muat Turun APK",
|
||||
ios: "IOS",
|
||||
iosCta: "Dapatkan di App Store",
|
||||
copyright: "© 2026 TalkPro. Hak cipta terpelihara.",
|
||||
@@ -1098,7 +1149,7 @@ export const translations = {
|
||||
description:
|
||||
"TalkPro をダウンロードして、よりクリーンでシンプル、モダンなつながり方を体験してください。",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK 近日公開",
|
||||
androidCta: "APK ダウンロード",
|
||||
ios: "IOS",
|
||||
iosCta: "App Store で入手",
|
||||
androidAlt: "Android",
|
||||
@@ -1118,7 +1169,7 @@ export const translations = {
|
||||
email: "email@hotmail.com",
|
||||
phone: "+01 123 45562334",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK 近日公開",
|
||||
androidCta: "APK ダウンロード",
|
||||
ios: "IOS",
|
||||
iosCta: "App Store で入手",
|
||||
copyright: "© 2026 TalkPro. All rights reserved.",
|
||||
@@ -1301,7 +1352,7 @@ export const translations = {
|
||||
description:
|
||||
"TalkPro를 다운로드하고 더 깔끔하고 단순하며 현대적인 연결 방식을 경험하세요.",
|
||||
android: "안드로이드",
|
||||
androidCta: "APK 곧 출시",
|
||||
androidCta: "APK 다운로드",
|
||||
ios: "IOS",
|
||||
iosCta: "App Store에서 받기",
|
||||
androidAlt: "Android",
|
||||
@@ -1321,7 +1372,7 @@ export const translations = {
|
||||
email: "email@hotmail.com",
|
||||
phone: "+01 123 45562334",
|
||||
android: "안드로이드",
|
||||
androidCta: "APK 곧 출시",
|
||||
androidCta: "APK 다운로드",
|
||||
ios: "IOS",
|
||||
iosCta: "App Store에서 받기",
|
||||
copyright: "© 2026 TalkPro. All rights reserved.",
|
||||
@@ -1356,7 +1407,7 @@ export const translations = {
|
||||
description:
|
||||
"Baixe o TalkPro e experimente uma forma mais limpa, simples e moderna de se conectar.",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK em breve",
|
||||
androidCta: "Baixar APK",
|
||||
ios: "IOS",
|
||||
iosCta: "Baixar na App Store",
|
||||
},
|
||||
@@ -1396,7 +1447,7 @@ export const translations = {
|
||||
description:
|
||||
"Lade TalkPro herunter und erlebe eine klarere, einfachere und modernere Art, verbunden zu bleiben.",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK bald verfügbar",
|
||||
androidCta: "APK herunterladen",
|
||||
ios: "IOS",
|
||||
iosCta: "Im App Store laden",
|
||||
},
|
||||
@@ -1436,7 +1487,7 @@ export const translations = {
|
||||
description:
|
||||
"Téléchargez TalkPro et découvrez une façon plus claire, simple et moderne de rester connecté.",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK bientôt disponible",
|
||||
androidCta: "Télécharger l'APK",
|
||||
ios: "IOS",
|
||||
iosCta: "Disponible sur l’App Store",
|
||||
},
|
||||
@@ -1476,7 +1527,7 @@ export const translations = {
|
||||
description:
|
||||
"TalkPro डाउनलोड करें और जुड़े रहने का साफ, सरल और आधुनिक तरीका अनुभव करें।",
|
||||
android: "एंड्रॉयड",
|
||||
androidCta: "APK जल्द आ रहा है",
|
||||
androidCta: "APK डाउनलोड",
|
||||
ios: "IOS",
|
||||
iosCta: "App Store से पाएं",
|
||||
},
|
||||
@@ -1516,7 +1567,7 @@ export const translations = {
|
||||
description:
|
||||
"نزّل TalkPro واختبر طريقة أوضح وأبسط وأكثر حداثة للبقاء على اتصال.",
|
||||
android: "أندرويد",
|
||||
androidCta: "APK قريبًا",
|
||||
androidCta: "تنزيل APK",
|
||||
ios: "آي أو إس",
|
||||
iosCta: "احصل عليه من App Store",
|
||||
},
|
||||
@@ -1556,7 +1607,7 @@ export const translations = {
|
||||
description:
|
||||
"Скачайте TalkPro и попробуйте более чистый, простой и современный способ общения.",
|
||||
android: "Андроид",
|
||||
androidCta: "APK скоро",
|
||||
androidCta: "Скачать APK",
|
||||
ios: "IOS",
|
||||
iosCta: "Загрузить в App Store",
|
||||
},
|
||||
@@ -1596,7 +1647,7 @@ export const translations = {
|
||||
description:
|
||||
"Unduh TalkPro dan rasakan cara yang lebih bersih, sederhana, dan modern untuk tetap terhubung.",
|
||||
android: "ANDROID",
|
||||
androidCta: "APK segera hadir",
|
||||
androidCta: "Unduh APK",
|
||||
ios: "IOS",
|
||||
iosCta: "Dapatkan di App Store",
|
||||
},
|
||||
@@ -1636,7 +1687,7 @@ export const translations = {
|
||||
description:
|
||||
"TalkPro ڈاؤن لوڈ کریں اور جڑے رہنے کا صاف، سادہ اور جدید طریقہ آزمائیں۔",
|
||||
android: "اینڈرائیڈ",
|
||||
androidCta: "APK جلد آ رہا ہے",
|
||||
androidCta: "APK ڈاؤن لوڈ",
|
||||
ios: "آئی او ایس",
|
||||
iosCta: "App Store سے حاصل کریں",
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ const {
|
||||
</head>
|
||||
<body class="bg-surface font-sans overflow-x-hidden">
|
||||
<slot />
|
||||
<script src="/site-links-client.js" defer></script>
|
||||
<script>
|
||||
(() => {
|
||||
const header = document.getElementById('site-header');
|
||||
|
||||
@@ -31,6 +31,6 @@ const t = getTranslations(lang)
|
||||
<UseCases t={t.useCases} />
|
||||
<Trust t={t.trust} />
|
||||
<!-- AppPreview section disabled per lead request. -->
|
||||
<DownloadCTA t={t.download} />
|
||||
<DownloadCTA t={t.download} siteLinks={t.siteLinks} />
|
||||
<Footer t={t.footer} />
|
||||
</Base>
|
||||
|
||||
@@ -24,6 +24,6 @@ const t = getTranslations(lang)
|
||||
<UseCases t={t.useCases} />
|
||||
<Trust t={t.trust} />
|
||||
<!-- AppPreview section disabled per lead request. -->
|
||||
<DownloadCTA t={t.download} />
|
||||
<DownloadCTA t={t.download} siteLinks={t.siteLinks} />
|
||||
<Footer t={t.footer} />
|
||||
</Base>
|
||||
|
||||
@@ -129,6 +129,21 @@
|
||||
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 {
|
||||
background: #f28a4b;
|
||||
border: 1px solid #c5834e;
|
||||
|
||||
Reference in New Issue
Block a user