Skip to content

Commit 11e0916

Browse files
Handle empty referral invite pages
1 parent cf47ea7 commit 11e0916

3 files changed

Lines changed: 155 additions & 13 deletions

File tree

docs/i/index.html

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
<div class="invite-layout">
4040
<section class="invite-hero" aria-labelledby="invite-title">
4141
<p class="eyebrow reveal" id="invite-title" data-t="invitedTitle">好友邀请你使用 PythonIDE</p>
42-
<div class="invite-code" data-invite-code aria-label="邀请码">PY8K29</div>
43-
<p class="summary reveal" data-t="invitedSummary">下载 App 后输入邀请码,即可建立邀请关系。</p>
42+
<div class="invite-code" data-invite-code aria-label="获取邀请码">获取邀请码</div>
43+
<p class="summary reveal" data-invite-summary data-t="invitedSummary">下载 App 后输入邀请码,即可建立邀请关系。</p>
4444

4545
<div class="actions reveal">
4646
<a class="button primary" href="#" data-download>
@@ -55,6 +55,12 @@
5555
</svg>
5656
<span data-t="copyCode">复制邀请码</span>
5757
</button>
58+
<a class="button secondary no-code-only" href="/activity/" data-no-code-rules hidden>
59+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
60+
<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"></path>
61+
</svg>
62+
<span data-t="viewRules">查看活动规则</span>
63+
</a>
5864
</div>
5965

6066
<div class="divider" aria-hidden="true"></div>

docs/referral/referral.css

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,29 @@ html[lang="en"] .language-toggle [data-lang="en"] {
261261
text-indent: 0.08em;
262262
}
263263

264+
.invite-code.invite-placeholder {
265+
font-family: var(--font);
266+
font-size: clamp(40px, 11vw, 62px);
267+
font-weight: 700;
268+
letter-spacing: 0;
269+
line-height: 1.12;
270+
text-indent: 0;
271+
white-space: normal;
272+
cursor: pointer;
273+
transition: opacity 220ms ease, transform 260ms var(--ease);
274+
}
275+
276+
.invite-code.invite-placeholder:hover {
277+
opacity: 0.78;
278+
transform: translateY(-1px);
279+
}
280+
281+
.invite-code.invite-placeholder:focus-visible {
282+
outline: 2px solid var(--ink);
283+
outline-offset: 8px;
284+
border-radius: 8px;
285+
}
286+
264287
.invite-code span {
265288
display: inline-block;
266289
animation: codeEnter 760ms var(--ease) both;
@@ -348,6 +371,15 @@ html[lang="en"] .language-toggle [data-lang="en"] {
348371
color: var(--ink);
349372
}
350373

374+
.no-code-only[hidden],
375+
[data-copy-code][hidden] {
376+
display: none;
377+
}
378+
379+
body.invite-missing-code .invite-hero > .inline-link {
380+
display: none;
381+
}
382+
351383
.divider {
352384
width: 100%;
353385
height: 1px;
@@ -665,23 +697,23 @@ html[lang="en"] .language-toggle [data-lang="en"] {
665697
background: linear-gradient(180deg, transparent, var(--line), transparent);
666698
}
667699

668-
.eyebrow,
669-
.summary,
670700
.activity-title,
671701
.note {
672702
text-align: left;
673703
margin-left: 0;
674704
}
675705

676706
.invite-code {
677-
margin-left: 0;
678-
text-align: left;
707+
margin-left: auto;
708+
margin-right: auto;
709+
text-align: center;
679710
text-indent: 0.18em;
680711
overflow: visible;
681712
}
682713

683714
.actions {
684-
margin-left: 0;
715+
margin-left: auto;
716+
margin-right: auto;
685717
}
686718

687719
.desktop-only {

docs/referral/referral.js

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const APP_STORE_URL = "https://apps.apple.com/app/id6753987304";
2+
const APP_REFERRAL_URL = "pythonide://referral";
23

34
const copy = {
45
zh: {
@@ -11,6 +12,10 @@ const copy = {
1112
failed: "复制失败,请手动复制",
1213
invitedTitle: "好友邀请你使用 PythonIDE",
1314
invitedSummary: "下载 App 后输入邀请码,即可建立邀请关系。",
15+
inviteEntryTitle: "PythonIDE 邀请",
16+
inviteEntrySummary: "打开 App 后进入「我的」-「邀请好友」,即可查看并分享你的专属邀请码。",
17+
getInviteCode: "获取邀请码",
18+
noCodeToCopy: "当前页面没有邀请码",
1419
howWorks: "如何生效",
1520
viewRules: "查看活动规则",
1621
ruleTitle: "邀请活动规则",
@@ -51,6 +56,10 @@ const copy = {
5156
failed: "Copy failed. Please copy manually.",
5257
invitedTitle: "A friend invited you to PythonIDE",
5358
invitedSummary: "Download the app and enter this invite code to bind the referral.",
59+
inviteEntryTitle: "PythonIDE Referral",
60+
inviteEntrySummary: "Open the app, then go to Me - Invite Friends to find and share your personal invite code.",
61+
getInviteCode: "Get Invite Code",
62+
noCodeToCopy: "No invite code on this page.",
5463
howWorks: "How it counts",
5564
viewRules: "View rules",
5665
ruleTitle: "Referral Program Rules",
@@ -102,15 +111,20 @@ function routePath() {
102111

103112
function inviteCodeFromPath() {
104113
const raw = routePath();
105-
const url = new URL(raw, location.origin);
114+
let url;
115+
try {
116+
url = new URL(raw, location.origin);
117+
} catch {
118+
return "";
119+
}
106120
const parts = url.pathname.split("/").filter(Boolean);
107121
const code = parts[0] === "i" && parts[1] ? parts[1] : url.searchParams.get("code");
108-
return normalizeCode(code || "PY8K29");
122+
return normalizeCode(code);
109123
}
110124

111125
function normalizeCode(value) {
112126
const cleaned = String(value || "").replace(/[\s-]+/g, "").toUpperCase();
113-
return /^[A-Z0-9]{4,12}$/.test(cleaned) ? cleaned : "PY8K29";
127+
return /^[A-Z0-9]{4,12}$/.test(cleaned) ? cleaned : "";
114128
}
115129

116130
function initialLanguage() {
@@ -121,6 +135,10 @@ function initialLanguage() {
121135
return navigator.language && navigator.language.toLowerCase().startsWith("en") ? "en" : "zh";
122136
}
123137

138+
function currentLanguage() {
139+
return document.documentElement.dataset.lang === "en" ? "en" : "zh";
140+
}
141+
124142
function setLanguage(lang) {
125143
const active = lang === "en" ? "en" : "zh";
126144
localStorage.setItem("referral_lang", active);
@@ -140,8 +158,7 @@ function setLanguage(lang) {
140158
document.querySelectorAll("[data-copy-feedback]").forEach((node) => {
141159
node.dataset.copyFeedback = t.copied;
142160
});
143-
const code = document.querySelector("[data-invite-code]")?.dataset.inviteCode || inviteCodeFromPath();
144-
renderInviteCode(code);
161+
applyInvitePageState(inviteCodeFromPath(), t);
145162
setupReveal();
146163
setupPointerMotion();
147164
}
@@ -208,15 +225,69 @@ function svgIcon(name) {
208225
function renderInviteCode(code) {
209226
const target = document.querySelector("[data-invite-code]");
210227
if (!target) return;
228+
if (!code) {
229+
renderInvitePlaceholder(copy[currentLanguage()].getInviteCode);
230+
return;
231+
}
211232
target.dataset.inviteCode = code;
212233
target.dataset.length = String(code.length);
234+
target.classList.remove("invite-placeholder");
213235
target.classList.toggle("compact", code.length >= 7);
214236
target.classList.toggle("ultra-compact", code.length >= 10);
237+
target.removeAttribute("role");
238+
target.removeAttribute("tabindex");
239+
target.setAttribute("aria-label", `${copy[currentLanguage()].invalidCode} ${code}`);
215240
target.innerHTML = code.split("").map((char, index) => `<span style="--i:${index}">${char}</span>`).join("");
216241
}
217242

243+
function renderInvitePlaceholder(label) {
244+
const target = document.querySelector("[data-invite-code]");
245+
if (!target) return;
246+
target.dataset.inviteCode = "";
247+
target.dataset.length = "0";
248+
target.classList.remove("compact", "ultra-compact");
249+
target.classList.add("invite-placeholder");
250+
target.setAttribute("role", "button");
251+
target.setAttribute("tabindex", "0");
252+
target.setAttribute("aria-label", label);
253+
target.textContent = label;
254+
}
255+
256+
function applyInvitePageState(code, t) {
257+
const target = document.querySelector("[data-invite-code]");
258+
if (!target) {
259+
document.body.classList.remove("invite-missing-code");
260+
return;
261+
}
262+
263+
const hasCode = Boolean(code);
264+
document.body.classList.toggle("invite-missing-code", !hasCode);
265+
266+
const title = document.getElementById("invite-title");
267+
if (title) title.textContent = hasCode ? t.invitedTitle : t.inviteEntryTitle;
268+
269+
const summary = document.querySelector("[data-invite-summary]");
270+
if (summary) summary.textContent = hasCode ? t.invitedSummary : t.inviteEntrySummary;
271+
272+
const copyButton = document.querySelector("[data-copy-code]");
273+
if (copyButton) copyButton.hidden = !hasCode;
274+
275+
const rulesButton = document.querySelector("[data-no-code-rules]");
276+
if (rulesButton) rulesButton.hidden = hasCode;
277+
278+
if (hasCode) {
279+
renderInviteCode(code);
280+
} else {
281+
renderInvitePlaceholder(t.getInviteCode);
282+
}
283+
}
284+
218285
async function copyInviteCode() {
219286
const code = document.querySelector("[data-invite-code]")?.dataset.inviteCode || inviteCodeFromPath();
287+
if (!code) {
288+
showToast(copy[currentLanguage()].noCodeToCopy);
289+
return;
290+
}
220291
try {
221292
await copyText(code);
222293
document.body.classList.add("code-copied");
@@ -232,6 +303,30 @@ async function copyInviteCode() {
232303
}
233304
}
234305

306+
function openReferralInApp() {
307+
const startedAt = Date.now();
308+
let didHide = false;
309+
const fallbackDelay = 900;
310+
const onVisibilityChange = () => {
311+
if (document.hidden) didHide = true;
312+
};
313+
314+
document.addEventListener("visibilitychange", onVisibilityChange, { once: true });
315+
window.setTimeout(() => {
316+
document.removeEventListener("visibilitychange", onVisibilityChange);
317+
if (!didHide && Date.now() - startedAt < 1800) {
318+
window.location.href = APP_STORE_URL;
319+
}
320+
}, fallbackDelay);
321+
322+
window.location.href = APP_REFERRAL_URL;
323+
}
324+
325+
function activateInvitePlaceholder() {
326+
if (!document.body.classList.contains("invite-missing-code")) return;
327+
openReferralInApp();
328+
}
329+
235330
async function copyText(text) {
236331
if (navigator.clipboard && window.isSecureContext) {
237332
await navigator.clipboard.writeText(text);
@@ -316,10 +411,19 @@ function setupPointerMotion() {
316411
}
317412

318413
function init() {
319-
renderInviteCode(inviteCodeFromPath());
320414
document.querySelectorAll("[data-download]").forEach((node) => {
321415
node.setAttribute("href", APP_STORE_URL);
322416
});
417+
const inviteCode = document.querySelector("[data-invite-code]");
418+
if (inviteCode) {
419+
inviteCode.addEventListener("click", activateInvitePlaceholder);
420+
inviteCode.addEventListener("keydown", (event) => {
421+
if (event.key !== "Enter" && event.key !== " ") return;
422+
if (!document.body.classList.contains("invite-missing-code")) return;
423+
event.preventDefault();
424+
openReferralInApp();
425+
});
426+
}
323427
document.querySelectorAll("[data-copy-code]").forEach((node) => {
324428
node.addEventListener("click", copyInviteCode);
325429
});

0 commit comments

Comments
 (0)