Skip to content

🎇 Happy Chinese New Year! #315

Description

@thekingofcity

今年,在过年的特殊期间,我希望一部分原先需要订阅的功能限时免费开放给所有登录用户

需要在首页添加活动通知,别忘了添加各语言欢度春节的翻译

参考以下代码实现逻辑,注意这是另一个项目的修改,不能直接套用,但是关键的alert使用和翻译可以参考

diff --git a/src/components/app-root.tsx b/src/components/app-root.tsx
index b996e168..995b0b20 100644
--- a/src/components/app-root.tsx
+++ b/src/components/app-root.tsx
@@ -5,9 +5,7 @@ import React from 'react';
 import { useTranslation } from 'react-i18next';
 import { LocalStorageKey } from '../constants/constants';
 import { useRootDispatch, useRootSelector } from '../redux';
-import { setActiveSubscriptions } from '../redux/account/account-slice';
-import { closePaletteAppClip, onPaletteAppClipEmit, setGlobalAlert } from '../redux/runtime/runtime-slice';
-import { checkCNY2026Period, CNY_CHECK_INTERVAL } from '../util/cny-2026';
+import { closePaletteAppClip, onPaletteAppClipEmit } from '../redux/runtime/runtime-slice';
 
 const PageHeader = React.lazy(() => import('./page-header/page-header'));
 const ToolsPanel = React.lazy(() => import('./panels/tools/tools'));
@@ -20,12 +18,7 @@ export default function AppRoot() {
     const {
         paletteAppClip: { input },
     } = useRootSelector(state => state.runtime);
-    const accountState = useRootSelector(state => state.account.state);
-    const activeSubscriptions = useRootSelector(state => state.account.activeSubscriptions);
-    const {
-        t,
-        i18n: { language: lang },
-    } = useTranslation();
+    const { t } = useTranslation();
 
     const [isShowRMTMessage, setIsShowRMTMessage] = React.useState(false);
 
@@ -35,61 +28,6 @@ export default function AppRoot() {
         }
     }, []);
 
-    // CNY 2026 promotion: Check if we're in the promotional period and unlock RMP_CLOUD
-    const [cnyNotificationShown, setCnyNotificationShown] = React.useState(false);
-
-    // Show CNY notification once per session (always, regardless of login or period)
-    React.useEffect(() => {
-        if (!cnyNotificationShown) {
-            const timeoutId = setTimeout(() => {
-                dispatch(
-                    setGlobalAlert({
-                        status: 'warning',
-                        message: t('happyChineseNewYear'),
-                        url: `https://railmapgen.org/rmt-blog/${lang}/rmg-7th-newyear`,
-                    })
-                );
-                setCnyNotificationShown(true);
-            }, 1000);
-            return () => clearTimeout(timeoutId);
-        }
-    }, [cnyNotificationShown, dispatch, t]);
-
-    // Check CNY period and enable RMP_CLOUD for logged-in users
-    React.useEffect(() => {
-        let isActive = true;
-
-        const checkAndApplyCNYPromotion = async () => {
-            // Check login status inside to handle users logging in during use
-            const isLoggedIn = accountState === 'free' || accountState === 'subscriber';
-            if (!isLoggedIn || !isActive) return;
-
-            const isInPeriod = await checkCNY2026Period();
-            if (isInPeriod && isActive) {
-                // Enable RMP_CLOUD for all logged-in users during CNY period
-                if (!activeSubscriptions.RMP_CLOUD) {
-                    dispatch(
-                        setActiveSubscriptions({
-                            ...activeSubscriptions,
-                            RMP_CLOUD: true,
-                        })
-                    );
-                }
-            }
-        };
-
-        // Check immediately on mount
-        checkAndApplyCNYPromotion();
-
-        // Check every 15 minutes
-        const intervalId = setInterval(checkAndApplyCNYPromotion, CNY_CHECK_INTERVAL);
-
-        return () => {
-            isActive = false;
-            clearInterval(intervalId);
-        };
-    }, [accountState, activeSubscriptions, dispatch]);
-
     return (
         <RmgThemeProvider>
             <RmgWindow>
diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json
index e0a1a62c..3e06fd79 100644
--- a/src/i18n/translations/en.json
+++ b/src/i18n/translations/en.json
@@ -950,7 +950,5 @@
         "flipDiagonal135": "Flip across 135° line"
     },
 
-    "localStorageQuotaExceeded": "Local storage limit reached. Unable to save new changes.",
-
-    "happyChineseNewYear": "🎇 Happy Chinese New Year 2026! Cloud features are free for all users during the festival period (Feb 15-23)."
+    "localStorageQuotaExceeded": "Local storage limit reached. Unable to save new changes."
 }
diff --git a/src/i18n/translations/ja.json b/src/i18n/translations/ja.json
index 78acedbb..3f45fc9e 100644
--- a/src/i18n/translations/ja.json
+++ b/src/i18n/translations/ja.json
@@ -951,7 +951,5 @@
         "flipDiagonal135": "135° 線で反転"
     },
 
-    "localStorageQuotaExceeded": "ローカルストレージの上限に達しました。新しい変更は保存できません。",
-
-    "happyChineseNewYear": "🎇 旧正月おめでとうございます!2026年春節期間中(2月15日~23日)、クラウド機能が全ユーザーに無料で開放されます。"
+    "localStorageQuotaExceeded": "ローカルストレージの上限に達しました。新しい変更は保存できません。"
 }
diff --git a/src/i18n/translations/ko.json b/src/i18n/translations/ko.json
index 11084f78..cfe3613d 100644
--- a/src/i18n/translations/ko.json
+++ b/src/i18n/translations/ko.json
@@ -949,7 +949,5 @@
         "flipDiagonal135": "135° 선을 기준으로 뒤집기"
     },
 
-    "localStorageQuotaExceeded": "로컬 저장소 한도에 도달했습니다. 새로운 변경 사항을 저장할 수 없습니다.",
-
-    "happyChineseNewYear": "🎇 새해 복 많이 받으세요! 2026년 춘절 기간(2월 15일~23일) 동안 모든 사용자에게 클라우드 기능이 무료로 제공됩니다."
+    "localStorageQuotaExceeded": "로컬 저장소 한도에 도달했습니다. 새로운 변경 사항을 저장할 수 없습니다."
 }
diff --git a/src/i18n/translations/zh-Hans.json b/src/i18n/translations/zh-Hans.json
index d35f8199..5374deb3 100644
--- a/src/i18n/translations/zh-Hans.json
+++ b/src/i18n/translations/zh-Hans.json
@@ -949,7 +949,5 @@
         "flipDiagonal135": "沿135°线翻转"
     },
 
-    "localStorageQuotaExceeded": "本地存储空间已达上限。无法保存新更改。",
-
-    "happyChineseNewYear": "🎇 恭贺新禧!2026年春节期间(2月15日至23日),云端功能向所有用户限时免费开放。"
+    "localStorageQuotaExceeded": "本地存储空间已达上限。无法保存新更改。"
 }
diff --git a/src/i18n/translations/zh-Hant.json b/src/i18n/translations/zh-Hant.json
index bf3020a9..1f9b5a01 100644
--- a/src/i18n/translations/zh-Hant.json
+++ b/src/i18n/translations/zh-Hant.json
@@ -949,7 +949,5 @@
         "flipDiagonal135": "沿135°線翻轉"
     },
 
-    "localStorageQuotaExceeded": "本機儲存空間已達上限。無法儲存新的變更。",
-
-    "happyChineseNewYear": "🎇 恭賀新禧!2026年春節期間(2月15日至23日),雲端功能向所有用戶限時免費開放。"
+    "localStorageQuotaExceeded": "本機儲存空間已達上限。無法儲存新的變更。"
 }
diff --git a/src/util/cny-2026.ts b/src/util/cny-2026.ts
deleted file mode 100644
index c97cae19..00000000
--- a/src/util/cny-2026.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { logger } from '@railmapgen/rmg-runtime';
-
-// Chinese New Year 2026 promotional period (UTC+8)
-// Start: Feb 15, 2026 00:00:00 UTC+8 = Feb 14, 2026 16:00:00 UTC
-// End: Feb 23, 2026 23:59:59 UTC+8 = Feb 23, 2026 15:59:59 UTC
-const CNY_START_UTC = new Date('2026-02-14T16:00:00Z').getTime();
-const CNY_END_UTC = new Date('2026-02-23T15:59:59Z').getTime();
-
-// Time server endpoints that return timestamps
-const TIME_SERVERS = ['https://worldtimeapi.org/api/ip', 'https://timeapi.io/api/Time/current/zone?timeZone=UTC'];
-
-interface WorldTimeAPIResponse {
-    unixtime?: number;
-    datetime?: string;
-}
-
-interface TimeAPIIOResponse {
-    dateTime?: string;
-}
-
-/**
- * Fetches the current time from multiple time servers.
- * Returns the first successful response as a Unix timestamp in milliseconds.
- * Returns 0 (epoch) if all servers fail - never uses local time to prevent manipulation.
- */
-export const fetchNetworkTime = async (): Promise<number> => {
-    for (const server of TIME_SERVERS) {
-        try {
-            const controller = new AbortController();
-            const timeoutId = setTimeout(() => controller.abort(), 5000);
-
-            const response = await fetch(server, {
-                signal: controller.signal,
-                cache: 'no-store',
-            });
-            clearTimeout(timeoutId);
-
-            if (!response.ok) continue;
-
-            const data = (await response.json()) as WorldTimeAPIResponse | TimeAPIIOResponse;
-
-            // worldtimeapi.org returns unixtime in seconds
-            if ('unixtime' in data && typeof data.unixtime === 'number') {
-                logger.debug(`CNY 2026: Got time from ${server}: ${data.unixtime * 1000}`);
-                return data.unixtime * 1000;
-            }
-
-            // timeapi.io returns dateTime as ISO string
-            if ('dateTime' in data && typeof data.dateTime === 'string') {
-                const timestamp = new Date(data.dateTime).getTime();
-                logger.debug(`CNY 2026: Got time from ${server}: ${timestamp}`);
-                return timestamp;
-            }
-        } catch (error) {
-            logger.debug(`CNY 2026: Failed to fetch time from ${server}: ${error}`);
-        }
-    }
-
-    // Never fallback to local time - return epoch (1970) to deny free access when network fails
-    logger.debug('CNY 2026: All time servers failed, returning epoch time');
-    return 0;
-};
-
-/**
- * Checks if the given timestamp is within the CNY 2026 promotional period.
- */
-export const isInCNY2026Period = (timestamp: number): boolean => {
-    return timestamp >= CNY_START_UTC && timestamp <= CNY_END_UTC;
-};
-
-/**
- * Checks if we are currently in the CNY 2026 promotional period.
- * Fetches time from network servers to avoid local time manipulation.
- */
-export const checkCNY2026Period = async (): Promise<boolean> => {
-    const currentTime = await fetchNetworkTime();
-    return isInCNY2026Period(currentTime);
-};
-
-// Check interval: 15 minutes in milliseconds
-export const CNY_CHECK_INTERVAL = 15 * 60 * 1000;

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions