Skip to content

Commit 5d0479c

Browse files
committed
feat: Add a global service notice component and update frontend dependencies.
1 parent 9bbb9b4 commit 5d0479c

6 files changed

Lines changed: 380 additions & 14 deletions

File tree

site/bun.lock

Lines changed: 22 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import { useEffect, useState } from 'react';
2+
3+
const GEO_ENDPOINT = 'https://api.country.is/';
4+
const DISMISS_KEY = 'pushy-global-service-notice-dismissed-v1';
5+
const GLOBAL_SERVICE_URL = 'https://cresc.dev';
6+
7+
type SupportedLocale =
8+
| 'de'
9+
| 'en'
10+
| 'es'
11+
| 'fr'
12+
| 'ja'
13+
| 'ko'
14+
| 'pt'
15+
| 'ru'
16+
| 'zh-Hans'
17+
| 'zh-Hant';
18+
19+
type NoticeCopy = {
20+
title: string;
21+
description: string;
22+
action: string;
23+
dismiss: string;
24+
};
25+
26+
const NOTICE_COPY: Record<SupportedLocale, NoticeCopy> = {
27+
de: {
28+
title: 'Greifen Sie außerhalb des chinesischen Festlands zu?',
29+
description:
30+
'Sie können zu cresc.dev wechseln und Cresc nutzen, den globalen Dienst, der getrennt von Pushy mit eigener Infrastruktur und eigenem Datenstandort betrieben wird.',
31+
action: 'Zu cresc.dev',
32+
dismiss: 'Bei Pushy bleiben',
33+
},
34+
en: {
35+
title: 'Outside mainland China?',
36+
description:
37+
'You can switch to cresc.dev for Cresc, the global service operated separately from Pushy with its own infrastructure and data location.',
38+
action: 'Go to cresc.dev',
39+
dismiss: 'Stay on Pushy',
40+
},
41+
es: {
42+
title: 'Estas fuera de China continental?',
43+
description:
44+
'Puedes ir a cresc.dev para usar Cresc, el servicio global operado por separado de Pushy con su propia infraestructura y ubicacion de datos.',
45+
action: 'Ir a cresc.dev',
46+
dismiss: 'Seguir en Pushy',
47+
},
48+
fr: {
49+
title: 'Vous etes en dehors de la Chine continentale ?',
50+
description:
51+
'Vous pouvez passer sur cresc.dev pour utiliser Cresc, le service mondial exploite separement de Pushy avec sa propre infrastructure et son propre stockage des donnees.',
52+
action: 'Aller sur cresc.dev',
53+
dismiss: 'Rester sur Pushy',
54+
},
55+
ja: {
56+
title: '中国本土以外からアクセスしていますか?',
57+
description:
58+
'cresc.dev に移動すると、Pushy とは別会社・別基盤で運営されるグローバル向けの Cresc サービスを利用できます。',
59+
action: 'cresc.dev へ移動',
60+
dismiss: 'Pushy を続ける',
61+
},
62+
ko: {
63+
title: '중국 본토 외 지역에서 접속 중이신가요?',
64+
description:
65+
'cresc.dev에서 Pushy와 별도 법인, 별도 인프라로 운영되는 글로벌 서비스 Cresc를 이용할 수 있습니다.',
66+
action: 'cresc.dev로 이동',
67+
dismiss: 'Pushy 계속 보기',
68+
},
69+
pt: {
70+
title: 'Voce esta fora da China continental?',
71+
description:
72+
'Voce pode ir para cresc.dev e usar o Cresc, o servico global operado separadamente do Pushy com sua propria infraestrutura e localizacao de dados.',
73+
action: 'Ir para cresc.dev',
74+
dismiss: 'Continuar no Pushy',
75+
},
76+
ru: {
77+
title: 'Вы находитесь за пределами материкового Китая?',
78+
description:
79+
'Вы можете перейти на cresc.dev и использовать Cresc, глобальный сервис с отдельной от Pushy компанией, инфраструктурой и размещением данных.',
80+
action: 'Перейти на cresc.dev',
81+
dismiss: 'Остаться на Pushy',
82+
},
83+
'zh-Hans': {
84+
title: '检测到你当前位于中国大陆以外地区',
85+
description:
86+
'你可以前往 cresc.dev,使用面向全球的 Cresc 服务。Cresc 与 Pushy 由不同公司实体独立运营,服务器与数据存放位置也彼此独立。',
87+
action: '前往 cresc.dev',
88+
dismiss: '继续浏览 Pushy',
89+
},
90+
'zh-Hant': {
91+
title: '檢測到你目前位於中國大陸以外地區',
92+
description:
93+
'你可以前往 cresc.dev,使用面向全球的 Cresc 服務。Cresc 與 Pushy 由不同公司實體獨立營運,伺服器與資料存放位置也彼此獨立。',
94+
action: '前往 cresc.dev',
95+
dismiss: '繼續瀏覽 Pushy',
96+
},
97+
};
98+
99+
function resolveLocale(): SupportedLocale {
100+
const locales =
101+
typeof navigator !== 'undefined'
102+
? navigator.languages?.length
103+
? navigator.languages
104+
: [navigator.language]
105+
: [];
106+
107+
for (const rawLocale of locales) {
108+
const locale = rawLocale.toLowerCase();
109+
if (locale.startsWith('zh')) {
110+
if (
111+
locale.includes('hant') ||
112+
locale.includes('tw') ||
113+
locale.includes('hk') ||
114+
locale.includes('mo')
115+
) {
116+
return 'zh-Hant';
117+
}
118+
return 'zh-Hans';
119+
}
120+
if (locale.startsWith('ja')) {
121+
return 'ja';
122+
}
123+
if (locale.startsWith('ko')) {
124+
return 'ko';
125+
}
126+
if (locale.startsWith('fr')) {
127+
return 'fr';
128+
}
129+
if (locale.startsWith('de')) {
130+
return 'de';
131+
}
132+
if (locale.startsWith('es')) {
133+
return 'es';
134+
}
135+
if (locale.startsWith('pt')) {
136+
return 'pt';
137+
}
138+
if (locale.startsWith('ru')) {
139+
return 'ru';
140+
}
141+
}
142+
143+
return 'en';
144+
}
145+
146+
function isDismissed() {
147+
try {
148+
return window.localStorage.getItem(DISMISS_KEY) === '1';
149+
} catch {
150+
return false;
151+
}
152+
}
153+
154+
function persistDismissed() {
155+
try {
156+
window.localStorage.setItem(DISMISS_KEY, '1');
157+
} catch {}
158+
}
159+
160+
export default function GlobalServiceNotice() {
161+
const [copy, setCopy] = useState<NoticeCopy>(NOTICE_COPY.en);
162+
const [visible, setVisible] = useState(false);
163+
164+
useEffect(() => {
165+
if (typeof window === 'undefined') {
166+
return;
167+
}
168+
169+
setCopy(NOTICE_COPY[resolveLocale()]);
170+
171+
if (isDismissed()) {
172+
return;
173+
}
174+
175+
let active = true;
176+
const controller = new AbortController();
177+
const timeoutId = window.setTimeout(() => controller.abort(), 4000);
178+
179+
const checkRegion = async () => {
180+
try {
181+
const response = await fetch(GEO_ENDPOINT, {
182+
cache: 'no-store',
183+
headers: {
184+
accept: 'application/json',
185+
},
186+
signal: controller.signal,
187+
});
188+
189+
if (!response.ok) {
190+
return;
191+
}
192+
193+
const data = (await response.json()) as { country?: string };
194+
if (active && data.country?.toUpperCase() && data.country.toUpperCase() !== 'CN') {
195+
setVisible(true);
196+
}
197+
} catch {
198+
} finally {
199+
window.clearTimeout(timeoutId);
200+
}
201+
};
202+
203+
void checkRegion();
204+
205+
return () => {
206+
active = false;
207+
controller.abort();
208+
window.clearTimeout(timeoutId);
209+
};
210+
}, []);
211+
212+
if (!visible) {
213+
return null;
214+
}
215+
216+
const handleDismiss = () => {
217+
persistDismissed();
218+
setVisible(false);
219+
};
220+
221+
return (
222+
<aside
223+
aria-live="polite"
224+
className="pushy-global-service-notice"
225+
role="status"
226+
>
227+
<div className="pushy-global-service-notice__body">
228+
<p className="pushy-global-service-notice__title">{copy.title}</p>
229+
<p className="pushy-global-service-notice__description">{copy.description}</p>
230+
</div>
231+
<div className="pushy-global-service-notice__actions">
232+
<a
233+
className="pushy-global-service-notice__primary"
234+
href={GLOBAL_SERVICE_URL}
235+
onClick={handleDismiss}
236+
>
237+
{copy.action}
238+
</a>
239+
<button
240+
className="pushy-global-service-notice__secondary"
241+
onClick={handleDismiss}
242+
type="button"
243+
>
244+
{copy.dismiss}
245+
</button>
246+
</div>
247+
</aside>
248+
);
249+
}

site/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
"dependencies": {
1313
"@ant-design/icons": "^6.1.0",
1414
"@rsbuild/plugin-sass": "^1.5.0",
15-
"@rspress/core": "^2.0.4",
15+
"@rspress/core": "^2.0.5",
1616
"@tailwindcss/postcss": "^4.2.1",
1717
"@types/react-dom": "^19.2.3",
18-
"antd": "^6.3.1",
18+
"antd": "^6.3.2",
1919
"autoprefixer": "10.4.27",
2020
"postcss": "^8.5.8",
2121
"react": "^19.2.4",

site/pages/docs/faq.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ type: 其他
1818

1919
#### 是否可以在海外使用?
2020

21-
可以的,国内外都有高速 CDN 节点。
21+
可以。Pushy 本身在国内外都部署有可用的 CDN 节点,能够满足大多数出海应用的热更新分发要求。
22+
23+
如果您的项目对海外合规、数据存放位置、运营主体隔离等方面还有额外要求,则可以考虑使用由不同公司实体独立运营的全球服务 [Cresc](https://cresc.dev/)
2224

2325
---
2426

site/styles/index.scss

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,100 @@ html:not(.dark) .rp-nav__title__logo-image--dark {
146146
html.dark .rp-nav__title__logo-image--light {
147147
display: none !important;
148148
}
149+
150+
.pushy-global-service-notice {
151+
position: fixed;
152+
right: 24px;
153+
bottom: 24px;
154+
z-index: 1200;
155+
width: min(420px, calc(100vw - 32px));
156+
border: 1px solid rgb(96 165 250 / 20%);
157+
border-radius: 20px;
158+
background:
159+
linear-gradient(135deg, rgb(15 23 42 / 96%), rgb(30 41 59 / 94%));
160+
box-shadow:
161+
0 24px 48px rgb(15 23 42 / 28%),
162+
0 8px 20px rgb(15 23 42 / 18%);
163+
color: #e2e8f0;
164+
backdrop-filter: blur(18px);
165+
}
166+
167+
.pushy-global-service-notice__body {
168+
padding: 18px 18px 14px;
169+
}
170+
171+
.pushy-global-service-notice__title {
172+
margin: 0;
173+
font-size: 16px;
174+
font-weight: 700;
175+
line-height: 1.4;
176+
color: #f8fafc;
177+
}
178+
179+
.pushy-global-service-notice__description {
180+
margin: 10px 0 0;
181+
font-size: 14px;
182+
line-height: 1.65;
183+
color: #cbd5e1;
184+
}
185+
186+
.pushy-global-service-notice__actions {
187+
display: flex;
188+
gap: 10px;
189+
padding: 0 18px 18px;
190+
}
191+
192+
.pushy-global-service-notice__primary,
193+
.pushy-global-service-notice__secondary {
194+
display: inline-flex;
195+
align-items: center;
196+
justify-content: center;
197+
min-height: 40px;
198+
padding: 0 14px;
199+
border-radius: 999px;
200+
font-size: 14px;
201+
font-weight: 600;
202+
text-decoration: none;
203+
transition:
204+
transform 160ms ease,
205+
background-color 160ms ease,
206+
border-color 160ms ease,
207+
color 160ms ease;
208+
}
209+
210+
.pushy-global-service-notice__primary {
211+
background: linear-gradient(135deg, #2563eb, #4f46e5);
212+
color: #fff;
213+
box-shadow: 0 12px 24px rgb(37 99 235 / 22%);
214+
}
215+
216+
.pushy-global-service-notice__primary:hover {
217+
color: #fff;
218+
transform: translateY(-1px);
219+
}
220+
221+
.pushy-global-service-notice__secondary {
222+
border: 1px solid rgb(148 163 184 / 28%);
223+
background: rgb(255 255 255 / 4%);
224+
color: #cbd5e1;
225+
cursor: pointer;
226+
}
227+
228+
.pushy-global-service-notice__secondary:hover {
229+
transform: translateY(-1px);
230+
border-color: rgb(148 163 184 / 40%);
231+
color: #f8fafc;
232+
}
233+
234+
@media (max-width: 640px) {
235+
.pushy-global-service-notice {
236+
right: 16px;
237+
bottom: 16px;
238+
left: 16px;
239+
width: auto;
240+
}
241+
242+
.pushy-global-service-notice__actions {
243+
flex-direction: column;
244+
}
245+
}

site/theme/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { Layout as BasicLayout } from '@rspress/core/theme-original';
2+
import GlobalServiceNotice from '../components/GlobalServiceNotice';
23
import '../styles/index.scss';
34
import '../components/home/home.scss';
45

5-
export const Layout = () => <BasicLayout />;
6+
export const Layout = () => (
7+
<>
8+
<GlobalServiceNotice />
9+
<BasicLayout />
10+
</>
11+
);
612
export * from '@rspress/core/theme-original';

0 commit comments

Comments
 (0)