diff --git a/src/components/menu/account-view/account-info.tsx b/src/components/menu/account-view/account-info.tsx index 98451db..da7ad69 100644 --- a/src/components/menu/account-view/account-info.tsx +++ b/src/components/menu/account-view/account-info.tsx @@ -45,7 +45,7 @@ const AccountInfo = () => { token ); if (rep.status === 401) { - showErrorToast(t('Login status expired')); + showErrorToast(t('Login status expired.')); dispatch(logout()); return; } @@ -68,7 +68,7 @@ const AccountInfo = () => { token ); if (rep.status === 401) { - showErrorToast(t('Login status expired')); + showErrorToast(t('Login status expired.')); dispatch(logout()); return; } diff --git a/src/components/menu/account-view/saves-section.tsx b/src/components/menu/account-view/saves-section.tsx index 1f64956..31f79bf 100644 --- a/src/components/menu/account-view/saves-section.tsx +++ b/src/components/menu/account-view/saves-section.tsx @@ -67,7 +67,7 @@ const SavesSection = () => { token ); if (rep.status === 401) { - showErrorToast(t('Login status expired')); + showErrorToast(t('Login status expired.')); dispatch(logout()); return; } @@ -94,7 +94,7 @@ const SavesSection = () => { // fetch cloud save metadata const savesRep = await dispatch(fetchSaveList()); if (savesRep.meta.requestStatus !== 'fulfilled') { - showErrorToast(t('Login status expired')); // TODO: also might be !200 response + showErrorToast(t('Login status expired.')); // TODO: also might be !200 response setSyncButtonIsLoading(undefined); return; } @@ -135,7 +135,7 @@ const SavesSection = () => { token ); if (rep.status === 401) { - showErrorToast(t('Login status expired')); + showErrorToast(t('Login status expired.')); setSyncButtonIsLoading(undefined); dispatch(logout()); return; @@ -151,7 +151,7 @@ const SavesSection = () => { setSyncButtonIsLoading(saveId); const rep = await apiFetch(API_ENDPOINT.SAVES + '/' + saveId, {}, token); if (rep.status === 401) { - showErrorToast(t('Login status expired')); + showErrorToast(t('Login status expired.')); setSyncButtonIsLoading(undefined); dispatch(logout()); return; @@ -174,7 +174,7 @@ const SavesSection = () => { setDeleteButtonIsLoading(saveId); const rep = await apiFetch(API_ENDPOINT.SAVES + '/' + currentSaveId, { method: 'DELETE' }, token); if (rep.status === 401) { - showErrorToast(t('Login status expired')); + showErrorToast(t('Login status expired.')); setDeleteButtonIsLoading(undefined); dispatch(logout()); return; @@ -195,7 +195,7 @@ const SavesSection = () => { token ); if (rep.status === 401) { - showErrorToast(t('Login status expired')); + showErrorToast(t('Login status expired.')); dispatch(logout()); return; } diff --git a/src/components/menu/account-view/subscription-section.tsx b/src/components/menu/account-view/subscription-section.tsx index 0374be3..095444a 100644 --- a/src/components/menu/account-view/subscription-section.tsx +++ b/src/components/menu/account-view/subscription-section.tsx @@ -1,70 +1,20 @@ +import { Button, Card, List, Stack, Text, Title, Tooltip } from '@mantine/core'; +import { RMSection, RMSectionBody, RMSectionHeader } from '@railmapgen/mantine-components'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { useRootDispatch, useRootSelector } from '../../../redux'; -import { - ActiveSubscriptions, - defaultActiveSubscriptions, - logout, - setActiveSubscriptions, -} from '../../../redux/account/account-slice'; -import { apiFetch } from '../../../util/api'; -import { API_ENDPOINT } from '../../../util/constants'; +import { useRootSelector } from '../../../redux'; import RedeemModal from '../../modal/redeem-modal'; -import { RMSection, RMSectionBody, RMSectionHeader } from '@railmapgen/mantine-components'; -import { Button, Card, List, Stack, Text, Title } from '@mantine/core'; -import { addNotification } from '../../../redux/notification/notification-slice'; -interface APISubscription { - type: 'RMP' | 'RMP_CLOUD' | 'RMP_EXPORT'; - expires: string; -} +const DAYS_TO_REMIND_RENEW = 90; +const MILLISECONDS_TO_REMIND_RENEW = DAYS_TO_REMIND_RENEW * 24 * 60 * 60 * 1000; const SubscriptionSection = () => { const { t } = useTranslation(); - const { isLoggedIn, token } = useRootSelector(state => state.account); - const dispatch = useRootDispatch(); - const [subscriptions, setSubscriptions] = React.useState([] as APISubscription[]); + const { activeSubscriptions } = useRootSelector(state => state.account); const [isRedeemModalOpen, setIsRedeemModalOpen] = React.useState(false); - const showErrorToast = (msg: string) => - dispatch( - addNotification({ - title: t('Unable to retrieve your subscriptions'), - message: msg, - type: 'error', - duration: 9000, - }) - ); - - const getSubscriptions = async () => { - if (!isLoggedIn) return; - const rep = await apiFetch(API_ENDPOINT.SUBSCRIPTION, {}, token); - if (rep.status === 401) { - showErrorToast(t('Login status expired')); - dispatch(logout()); - return; - } - if (rep.status !== 200) { - showErrorToast(await rep.text()); - return; - } - const subscriptions = (await rep.json()).subscriptions as APISubscription[]; - if (!subscriptions.map(_ => _.type).includes('RMP_CLOUD')) return; - setSubscriptions([{ type: 'RMP', expires: subscriptions[0].expires }]); - - const activeSubscriptions = structuredClone(defaultActiveSubscriptions); - for (const subscription of subscriptions) { - const type = subscription.type; - if (type in activeSubscriptions) { - activeSubscriptions[type as keyof ActiveSubscriptions] = true; - } - } - dispatch(setActiveSubscriptions(activeSubscriptions)); - }; - React.useEffect(() => { - getSubscriptions(); - }, []); + const noneSubscription = !activeSubscriptions.RMP_CLOUD && !activeSubscriptions.RMP_EXPORT; return ( @@ -78,12 +28,10 @@ const SubscriptionSection = () => { - {subscriptions.length === 0 && ( + {noneSubscription && ( - - {t('Rail Map Painter')} - + {t('Rail Map Painter')} {t('With this subscription, the following features are unlocked:')} @@ -92,6 +40,7 @@ const SubscriptionSection = () => { {t('Sync 9 more saves')} {t('Unlimited master nodes')} {t('Unlimited parallel lines')} + {t('Random station names')} @@ -101,30 +50,70 @@ const SubscriptionSection = () => { )} - {subscriptions.map(_ => ( - + {!noneSubscription && ( + - - {t(_.type === 'RMP' ? 'Rail Map Painter' : _.type)} - + {t('Rail Map Painter')} - {t('Expires at:')} {new Date(_.expires).toLocaleString()} + {t('Expires at:')} {new Date(activeSubscriptions.RMP_CLOUD!).toLocaleString()} - + {new Date(activeSubscriptions.RMP_CLOUD!).getTime() - new Date().getTime() < + MILLISECONDS_TO_REMIND_RENEW ? ( + + + + ) : ( + + )} - ))} + )} + + {/* {Object.entries(activeSubscriptions) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([_, expires]) => expires) + .map(([type, expires]) => ( + + + {t(type)} + + + + {t('Expires at:')} {new Date(expires).toLocaleString()} + + {new Date(expires).getTime() - new Date().getTime() < MILLISECONDS_TO_REMIND_RENEW ? ( + + + + ) : ( + + )} + + + ))} */} - setIsRedeemModalOpen(false)} - getSubscriptions={getSubscriptions} - /> + setIsRedeemModalOpen(false)} /> ); }; diff --git a/src/components/modal/redeem-modal.tsx b/src/components/modal/redeem-modal.tsx index dbb57a7..c62e504 100644 --- a/src/components/modal/redeem-modal.tsx +++ b/src/components/modal/redeem-modal.tsx @@ -1,16 +1,16 @@ +import { Anchor, Button, Group, List, Modal, Text, TextInput } from '@mantine/core'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { MdOpenInNew } from 'react-icons/md'; import { useRootDispatch, useRootSelector } from '../../redux'; -import { logout } from '../../redux/account/account-slice'; +import { fetchSubscription, logout } from '../../redux/account/account-slice'; +import { addNotification } from '../../redux/notification/notification-slice'; import { apiFetch } from '../../util/api'; import { API_ENDPOINT } from '../../util/constants'; -import { Anchor, Button, Group, List, Modal, Text, TextInput } from '@mantine/core'; -import { addNotification } from '../../redux/notification/notification-slice'; -const RedeemModal = (props: { opened: boolean; onClose: () => void; getSubscriptions: () => Promise }) => { +const RedeemModal = (props: { opened: boolean; onClose: () => void }) => { const { t } = useTranslation(); - const { opened, onClose, getSubscriptions } = props; + const { opened, onClose } = props; const { isLoggedIn, token } = useRootSelector(state => state.account); const dispatch = useRootDispatch(); @@ -34,7 +34,7 @@ const RedeemModal = (props: { opened: boolean; onClose: () => void; getSubscript token ); if (rep.status === 401) { - showErrorToast(t('Login status expired')); + showErrorToast(t('Login status expired.')); dispatch(logout()); return; } @@ -43,7 +43,7 @@ const RedeemModal = (props: { opened: boolean; onClose: () => void; getSubscript showErrorToast(t(msg)); return; } - await getSubscriptions(); + dispatch(fetchSubscription()); // TODO: let RMP refresh its subscription status onClose(); }; diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 9758994..8c763a9 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -21,5 +21,8 @@ "text2": " from local storage. This item is created and used by ", "text3": ". Deleting it may cause unrecoverable data loss or unexpected behaviour to the applications.", "text4": "Please close this modal unless you're certain to do so or are supervised by our developers." - } + }, + + "RMP_CLOUD": "Rail Map Painter・Tianshu", + "RMP_EXPORT": "Rail Map Painter・Yuheng" } \ No newline at end of file diff --git a/src/i18n/translations/ja.json b/src/i18n/translations/ja.json index 584e3c0..7eb57e3 100644 --- a/src/i18n/translations/ja.json +++ b/src/i18n/translations/ja.json @@ -16,117 +16,145 @@ "safari": "Safariでは使用できません ☹️" }, + "LocalStorageModal": { + "text1": "ローカルストレージから ", + "text2": " を削除しようとしています。このプロジェクトは ", + "text3": " によって作成および使用されました。削除すると、データが失われて復元できなくなったり、アプリケーションが異常終了したりする可能性があります。", + "text4": "ご不明な場合、または当社の開発者の指示なしに操作している場合は、このダイアログを閉じてください。" + }, + "About": "概要", - "Allow cookies to help improve our website": "クッキーを許可して網頁の改善に協力する", + "Allow cookies to help improve our website": "Cookie を許可して網頁の改善にご協力ください", "Appearance": "外観", "Back to production environment": "本番環境に戻る", "Close all tabs": "すべてのタブを閉じる", "Close app": "アプリを閉じる", "Close current tab": "現在のタブを閉じる", "Close tab": "タブを閉じる", - "Contribution Wiki": "コントリビューションガイド", + "Contribution Wiki": "貢献者ガイド", "Contributors": "貢献者", "Current session has been terminated. Please close this window.": "現在のセッションは終了しました。このウィンドウを閉じてください。", - "Dark": "ダーク", + "Dark": "闇", + "Delete and reload": "削除して再読み込み", "Developer team": "開発チーム", "Devtools": "開発者ツール", - "Export Local Storage": "ローカルストレージのエクスポート", + "Export Local Storage": "ローカルストレージをエクスポート", "Donators": "寄付者", "Don't show again": "再度表示しない", "Download desktop app": "デスクトップアプリをダウンロード", - "Fonts are slow to load? Learn how to speed it up!": "フォントの読み込みが遅い?スピードアップの方法を学びましょう!", - "Official website": "公式網頁", + "Follow us on Bilibili": "Bilibili でフォローしてください", + "Fonts are slow to load? Learn how to speed it up!": "フォントの読み込みが遅いですか?高速化する方法をご覧ください!", "GitHub Pages mirror": "GitHub Pages ミラー", "GitLab Pages mirror": "GitLab Pages ミラー", "Happy Chinese New Year!": "新年あけましておめでとうございます!", "Help & support": "助けと支援", - "Join us on Slack": "Slackに参加する", - "Follow us on Bilibili": "ビリビリでフォローしてください", - "Light": "ライト", + "Includes at least 8 characters": "8 文字以上を含める", + "Join us on Slack": "Slack に参加してください", + "Light": "光", + "Login success": "ログイン成功", "Main languages": "主要言語", - "More": "もっと見る", - "More mirrors": "他のミラー", + "More": "その他", + "More mirrors": "その他のミラー", "New tab": "新しいタブ", - "Notes: Contributors are sorted by number of commits and commit time.": "注:貢献者はコミット数とコミット時間に基づいて並べられています。", - "OK": "OK", - "Other languages": "他の言語", - "Pro tip": "プロのコツ", - "Rail Map Toolkit is opened in another window": "レールマップツールキットは別のウィンドウで開かれています", - "Raise an Issue on GitHub": "GitHubにIssueを報告", - "Refreshing is required for changes to take effect.": "変更を適用するには再読み込みが必要です。", + "Notes: Contributors are sorted by number of commits and commit time.": "注:貢献者はコミット数とコミット時間によって並べ替えられます。", + "Official website": "公式網頁", + "Other languages": "その他の言語", + "Pro tip": "プロのヒント", + "Rail Map Toolkit is opened in another window": "地下鉄路線図工具一式は別のウィンドウで開かれています", + "Raise an Issue on GitHub": "GitHub で Issue を作成", + "Refreshing is required for changes to take effect.": "変更を有効にするには更新が必要です。", + "Remove this item": "このアイテムを削除", "Resource administrators": "リソース管理者", "Resource contributors": "リソース提供者", - "Restart RMT in this window": "このウィンドウでRMTを再起動する", + "Restart RMT in this window": "このウィンドウで地下鉄路線図工具一式を再起動", "Running": "実行中", "Save": "保存", - "Select an app to start your own rail map design!": "アプリを選択して自分のレールマップデザインを始めましょう!", - "Show dev tools for 1 day": "1日間開発ツールを表示", + "Select an app to start your own rail map design!": "アプリを選択して、独自の路線図デザインを始めましょう!", + "Show dev tools for 1 day": "開発者ツールを 1 日間表示", "Switch": "切り替え", - "Switch to": "切り替え", + "Switch to": "切り替え先", "System": "システムに従う", "Tab": "タブ", "Terms and conditions": "利用規約", "Tutorial": "チュートリアル", "Official Website": "公式網頁", "Mini Metro Web": "ミニ地下鉄描画ツール", - "Unable to load contributors": "貢献者を読み込めません", + "Unable to create your account": "アカウントを作成できません", + "Unable to reset your password": "パスワードをリセットできません", + "Unable to load contributors": "コントリビューターを読み込めません", "Unable to load donators": "寄付者を読み込めません", + "Unable to login": "ログインできません", + "Unable to redeem": "引き換えできません", + "Unable to retrieve your subscriptions": "契約を取得できません", + "Unable to sync your save": "保存を同期できません", + "Unable to update account info": "アカウント情報を更新できません", + "Unknown app": "不明なアプリ", "Useful links": "便利なリンク", "Visit GitHub": "GitHubを訪問", "Web fonts": "ウェブフォント", - "Welcome to Rail Map Toolkit": "レールマップツールキットへようこそ", - "You cannot open multiple Rail Map Toolkit at the same time. Please close this window.": "複数のレールマップツールキットを同時に開くことはできません。このウィンドウを閉じてください。", + "Welcome to Rail Map Toolkit": "地下鉄路線図工具一式へようこそ", + "We'll never share your email.": "あなたのメールアドレスを共有することはありません。", + "You cannot open multiple Rail Map Toolkit at the same time. Please close this window.": "同時に複数の地下鉄路線図工具一式を開くことはできません。このウィンドウを閉じてください。", + "Your account is created successfully.": "アカウントは正常に作成されました。", + "Your password has been reset successfully.": "パスワードは正常にリセットされました。", "You're currently viewing a testing environment.": "現在、テスト環境を閲覧しています。", - "Login status expired": "ログイン状態が期限切れです", - "Log in / Sign up": "ログイン / サインアップ", + "Login status expired.": "ログイン状態の有効期限が切れました。", + "Log in / Sign up": "ログイン / 新規登録", "Log out": "ログアウト", - "Change password": "パスワードを変更", - "Update account info": "アカウント情報を更新", + "Change password": "パスワードの変更", + "Update account info": "アカウント情報の更新", "Change": "変更", "Account": "アカウント", - "Welcome": "ようこそ", - "Forgot password": "パスワードを忘れた", - "Check your email again!": "メールを再確認してください!", - "Email with reset link is sent to": "リセットリンクを含むメールが送信されました", + "Welcome": "ようこそ ", + "Forgot password": "パスワードをお忘れですか", + "Check your email again!": "もう一度メールアドレスを確認してください!", + "Email with reset link is sent to": "リセットリンク付きのメールが送信されました:", "Email": "メールアドレス", "Send reset link": "リセットリンクを送信", + "Reset link sent": "リセットリンク送信済み", "Reset password token": "パスワードリセットトークン", "Password": "パスワード", "Reset password": "パスワードをリセット", "Back to log in": "ログインに戻る", "Log in": "ログイン", - "Sign up": "サインアップ", + "Create an account": "アカウントを作成", + "Sign up": "新規登録", "The email is not valid!": "メールアドレスが無効です!", - "Verification email is sent to": "確認メールが送信されました", - "Name": "名前", + "Verification email is sent to": "確認メールが送信されました:", "You may always change it later.": "後でいつでも変更できます。", "Send verification code": "確認コードを送信", + "Verification code sent": "確認コード送信済み", "Verification code": "確認コード", - "Failed to get the RMP save!": "RMP保存を取得できませんでした!", + "Minimum 8 characters. Contain at least 1 letter and 1 number.": "8 文字以上。少なくとも 1 つの文字と 1 つの数字を含めてください。", + "Failed to get the RMP save!": "RMP 保存の取得に失敗しました!", "Can not sync this save!": "この保存を同期できません!", - "Synced saves": "同期された保存", - "Subscribe to sync more": "購読でより多く同期", + "Synced saves": "同期済み保存", + "Subscribe to sync more": "購読してさらに同期", "Create": "作成", "Current save": "現在の保存", - "Cloud save": "クラウド保存", + "Cloud save": "雲保存", "ID": "一意の識別子", "Status": "状態", "Last update at": "最終更新日:", - "Delete ": "削除", + "Delete": "削除", "Sync now": "今すぐ同期", "Sync this slot": "このスロットを同期", - "All subscriptions": "すべての購読", + "All subscriptions": "すべての契約", "Redeem": "引き換え", + "RMP_CLOUD": "地下鉄路線図画家・天枢", + "RMP_EXPORT": "地下鉄路線図画家・玉衡", "With this subscription, the following features are unlocked:": "この購読で次の機能が解放されます:", - "PRO features": "PRO機能", - "Sync 9 more saves": "追加の9つの保存を同期", + "PRO features": "プロ機能", + "Sync 9 more saves": "さらに 9 つの保存を同期", "Unlimited master nodes": "大師節点無制限", "Unlimited parallel lines": "平行路線無制限", - "Expires at:": "期限:", + "Random station names": "無作為の駅名", + "Expires at:": "有効期限:", "Not applicable": "該当なし", "Renew": "更新", - "Redeem your subscription": "購読を引き換え", + "Renew now and get an extra 45 days!": "今すぐ更新してさらに 45 日間延長!", + "Redeem your subscription": "契約を引き換える", "CDKey could be purchased in the following sites:": "CD鍵は以下の網頁で購入できます:", "Enter your CDKey here": "ここにCD鍵を入力", diff --git a/src/i18n/translations/ko.json b/src/i18n/translations/ko.json index 87a633d..dff86c0 100644 --- a/src/i18n/translations/ko.json +++ b/src/i18n/translations/ko.json @@ -1,139 +1,168 @@ { "CookiesModal": { - "header": "본 사이트의 쿠키", - "text1": "RMG에서는 본 사이트 및 서비스가 정상적으로 작동하도록 쿠키를 사용합니다. 이러한 쿠키는 필수적이며 기본적으로 활성화되어 있습니다.", - "text2": "또한 우리는 다음과 같은 방법으로 쿠키를 사용합니다:", - "item1": "사이트 개선을 위해 사용자의 사용 패턴을 분석합니다.", - "text3": "이 쿠키들은 선택 사항입니다. 모든 선택적 쿠키를 허용하려면 '모든 쿠키 허용'을 선택하세요. 쿠키를 비활성화하려면 '거부'를 선택하세요.", - "accept": "모든 쿠키 허용", + "header": "본 웹사이트의 쿠키", + "text1": "RMG에서는 본 웹사이트 및 서비스가 정상적으로 작동하도록 쿠키를 사용합니다. 이러한 쿠키는 필수적이므로 기본적으로 활성화되어 있습니다.", + "text2": "또한 다음과 같은 방식으로 쿠키를 사용합니다.", + "item1": "귀하의 사용 패턴을 분석하여 본 웹사이트를 개선합니다.", + "text3": "이러한 쿠키는 선택 사항입니다. 모든 선택적 쿠키를 허용하려면 '모든 쿠키 수락'을 선택하십시오. 쿠키를 비활성화하려면 '거부'를 선택하십시오.", + "accept": "모든 쿠키 수락", "reject": "거부" }, "FontsSection": { - "text1": "프로젝트에서 사용하는 다음 글꼴은 사용자의 장치가 아닌 우리의 서버에서 로드됩니다.", - "text2": "이로 인해 불필요한 네트워크 트래픽이 발생할 수 있으며, 연결이 불안정할 경우 로딩이 느려질 수 있습니다.", - "text3": "이 오픈 소스 무료 글꼴을 다운로드하고 설치하여 사용 경험을 향상시킬 수 있습니다.", - "safari": "Safari에서는 지원되지 않음 ☹️" + "text1": "귀하의 프로젝트에 사용된 다음 글꼴은 귀하의 장치가 아닌 당사 서버에서 로드됩니다.", + "text2": "이로 인해 불필요한 네트워크 트래픽이 발생하거나 연결이 불안정한 경우 로딩 속도가 느려질 수 있습니다.", + "text3": "이러한 오픈 소스 무료 글꼴을 다운로드하여 설치하면 사용 환경을 개선할 수 있습니다.", + "safari": "Safari에는 적용되지 않음 ☹️" + }, + + "LocalStorageModal": { + "text1": "로컬 저장소에서 ", + "text2": " 을(를) 삭제하려고 합니다. 이 프로젝트는 ", + "text3": " 에서 만들고 사용했습니다. 삭제하면 데이터를 복구할 수 없게 되거나 애플리케이션이 오작동할 수 있습니다.", + "text4": "확실하지 않거나 당사 개발자의 안내 없이 작업하는 경우 이 대화 상자를 닫으십시오." }, "About": "정보", "Allow cookies to help improve our website": "쿠키를 허용하여 웹사이트 개선에 도움을 주세요", - "Appearance": "외관", + "Appearance": "모양", "Back to production environment": "프로덕션 환경으로 돌아가기", "Close all tabs": "모든 탭 닫기", "Close app": "앱 닫기", "Close current tab": "현재 탭 닫기", "Close tab": "탭 닫기", - "Contribution Wiki": "기여자 안내서", + "Contribution Wiki": "기여자 가이드", "Contributors": "기여자", - "Current session has been terminated. Please close this window.": "현재 세션이 종료되었습니다. 이 창을 닫아주세요.", - "Dark": "어두운 모드", - "Developer team": "개발자 팀", + "Current session has been terminated. Please close this window.": "현재 세션이 종료되었습니다. 이 창을 닫으십시오.", + "Dark": "어둡게", + "Delete and reload": "삭제 및 새로고침", + "Developer team": "개발팀", "Devtools": "개발자 도구", "Export Local Storage": "로컬 저장소 내보내기", "Donators": "기부자", - "Don't show again": "다시 보지 않기", - "Download desktop app": "데스크탑 앱 다운로드", + "Don't show again": "다시 표시 안 함", + "Download desktop app": "데스크톱 앱 다운로드", + "Follow us on Bilibili": "Bilibili에서 저희를 팔로우하세요", "Fonts are slow to load? Learn how to speed it up!": "글꼴 로딩이 느린가요? 속도를 높이는 방법을 알아보세요!", - "Official website": "공식 웹사이트", "GitHub Pages mirror": "GitHub Pages 미러", "GitLab Pages mirror": "GitLab Pages 미러", "Happy Chinese New Year!": "새해 복 많이 받으세요!", "Help & support": "도움말 및 지원", - "Join us on Slack": "Slack에 참여하기", - "Follow us on Bilibili": "빌리빌리에서 팔로우해주세요", - "Light": "밝은 모드", + "Includes at least 8 characters": "최소 8자 이상 포함", + "Join us on Slack": "Slack 그룹에 참여하세요", + "Light": "밝게", + "Login success": "로그인 성공", "Main languages": "주요 언어", - "More": "더보기", + "More": "더 보기", "More mirrors": "더 많은 미러", "New tab": "새 탭", "Notes: Contributors are sorted by number of commits and commit time.": "참고: 기여자는 커밋 수와 커밋 시간을 기준으로 정렬됩니다.", - "OK": "확인", + "Official website": "공식 웹사이트", "Other languages": "기타 언어", - "Pro tip": "팁", - "Rail Map Toolkit is opened in another window": "철도 노선도 도구가 다른 창에서 열려 있습니다.", - "Raise an Issue on GitHub": "GitHub에서 이슈 제기", - "Refreshing is required for changes to take effect.": "변경 사항이 적용되려면 새로 고침이 필요합니다.", - "Resource administrators": "자원 관리자", - "Resource contributors": "자원 기여자", - "Restart RMT in this window": "이 창에서 RMT 재시작", + "Pro tip": "전문가 팁", + "Rail Map Toolkit is opened in another window": "노선도 툴킷이 다른 창에서 열렸습니다.", + "Raise an Issue on GitHub": "GitHub에 문제 제기", + "Refreshing is required for changes to take effect.": "변경 사항을 적용하려면 새로고침해야 합니다.", + "Remove this item": "이 항목 제거", + "Resource administrators": "리소스 관리자", + "Resource contributors": "리소스 기여자", + "Restart RMT in this window": "이 창에서 RMT 다시 시작", "Running": "실행 중", "Save": "저장", - "Select an app to start your own rail map design!": "앱을 선택하여 나만의 철도 노선도 디자인을 시작하세요!", + "Select an app to start your own rail map design!": "앱을 선택하여 나만의 노선도 디자인을 시작하세요!", "Show dev tools for 1 day": "1일 동안 개발자 도구 표시", "Switch": "전환", - "Switch to": "로 전환", - "System": "시스템 설정 따르기", + "Switch to": "다음으로 전환", + "System": "시스템 설정 따름", "Tab": "탭", "Terms and conditions": "이용 약관", - "Tutorial": "튜토리얼", + "Tutorial": "사용 설명서", "Official Website": "공식 웹사이트", - "Mini Metro Web": "미니 지하철 그리기 도구", - "Unable to load contributors": "기여자 목록을 불러올 수 없습니다", + "Mini Metro Web": "미니 메트로 웹", + "Unable to create your account": "계정을 만들 수 없습니다.", + "Unable to reset your password": "비밀번호를 재설정할 수 없습니다.", + "Unable to load contributors": "기여자 목록을 불러올 수 없습니다.", + "Unable to load donators": "기부자 목록을 불러올 수 없습니다.", + "Unable to login": "로그인할 수 없습니다.", + "Unable to redeem": "교환할 수 없습니다.", + "Unable to retrieve your subscriptions": "구독 정보를 가져올 수 없습니다.", + "Unable to sync your save": "저장 파일을 동기화할 수 없습니다.", + "Unable to update account info": "계정 정보를 업데이트할 수 없습니다.", + "Unknown app": "알 수 없는 앱", "Useful links": "유용한 링크", "Visit GitHub": "GitHub 방문", "Web fonts": "웹 글꼴", - "Welcome to Rail Map Toolkit": "철도 노선도 도구에 오신 것을 환영합니다", - "You cannot open multiple Rail Map Toolkit at the same time. Please close this window.": "여러 철도 노선도 도구를 동시에 열 수 없습니다. 이 창을 닫아주세요.", - "You're currently viewing a testing environment.": "현재 테스트 환경을 보고 있습니다.", + "Welcome to Rail Map Toolkit": "지하철 노선도 툴킷에 오신 것을 환영합니다", + "We'll never share your email.": "귀하의 이메일 주소는 절대 공유하지 않습니다.", + "You cannot open multiple Rail Map Toolkit at the same time. Please close this window.": "동시에 여러 개의 노선도 툴킷을 열 수 없습니다. 이 창을 닫으십시오.", + "Your account is created successfully.": "계정이 성공적으로 만들어졌습니다.", + "Your password has been reset successfully.": "비밀번호가 성공적으로 재설정되었습니다.", + "You're currently viewing a testing environment.": "현재 테스트 환경을 보고 계십니다.", - "Login status expired": "로그인 상태가 만료되었습니다", - "Log in / Sign up": "로그인 / 가입", + "Login status expired.": "로그인 상태가 만료되었습니다.", + "Log in / Sign up": "로그인 / 회원가입", "Log out": "로그아웃", "Change password": "비밀번호 변경", - "Update account info": "계정 정보 업데이트", + "Update account info": "계정 정보 수정", "Change": "변경", "Account": "계정", - "Welcome": "환영합니다", + "Welcome": "환영합니다 ", "Forgot password": "비밀번호를 잊으셨나요", - "Check your email again!": "이메일을 다시 확인하세요!", - "Email with reset link is sent to": "비밀번호 재설정 링크가 이메일로 전송되었습니다", + "Check your email again!": "이메일 주소를 다시 확인하세요!", + "Email with reset link is sent to": "비밀번호 재설정 링크가 포함된 이메일이 다음 주소로 전송되었습니다:", "Email": "이메일 주소", - "Send reset link": "재설정 링크 전송", + "Send reset link": "재설정 이메일 보내기", + "Reset link sent": "재설정 이메일 전송됨", "Reset password token": "비밀번호 재설정 토큰", "Password": "비밀번호", "Reset password": "비밀번호 재설정", "Back to log in": "로그인으로 돌아가기", "Log in": "로그인", - "Sign up": "가입", + "Create an account": "새 계정 만들기", + "Sign up": "회원가입", "The email is not valid!": "이메일 주소가 유효하지 않습니다!", - "Verification email is sent to": "인증 이메일이 전송되었습니다", - "Name": "이름", + "Verification email is sent to": "인증 이메일이 다음 주소로 전송되었습니다:", "You may always change it later.": "나중에 언제든지 변경할 수 있습니다.", - "Send verification code": "인증 코드 전송", + "Send verification code": "인증 코드 보내기", + "Verification code sent": "인증 코드 전송됨", "Verification code": "인증 코드", - "Failed to get the RMP save!": "RMP 저장을 가져오지 못했습니다!", - "Can not sync this save!": "이 저장을 동기화할 수 없습니다!", - "Synced saves": "동기화된 저장", - "Subscribe to sync more": "더 많은 동기화를 위해 구독하세요", - "Create": "생성", - "Current save": "현재 저장", - "Cloud save": "클라우드 저장", + "Minimum 8 characters. Contain at least 1 letter and 1 number.": "8자 이상, 최소 1개의 문자와 1개의 숫자를 포함해야 합니다.", + "Failed to get the RMP save!": "RMP 저장 파일을 가져오는 데 실패했습니다!", + "Can not sync this save!": "이 저장 파일을 동기화할 수 없습니다!", + "Synced saves": "동기화된 저장 파일", + "Subscribe to sync more": "더 많이 동기화하려면 구독하세요", + "Create": "만들기", + "Current save": "현재 저장 파일", + "Cloud save": "클라우드 저장 파일", "ID": "고유 식별자", "Status": "상태", - "Last update at": "마지막 업데이트:", + "Last update at": "마지막 업데이트", "Delete": "삭제", "Sync now": "지금 동기화", "Sync this slot": "이 슬롯 동기화", "All subscriptions": "모든 구독", "Redeem": "교환", - "With this subscription, the following features are unlocked:": "이 구독으로 다음 기능이 잠금 해제됩니다:", - "PRO features": "PRO 기능", - "Sync 9 more saves": "추가 9개 저장 동기화", + "RMP_CLOUD": "지하철 노선도 툴킷・천추", + "RMP_EXPORT": "지하철 노선도 툴킷・옥형", + "With this subscription, the following features are unlocked:": "이 구독을 통해 다음 기능이 잠금 해제됩니다:", + "PRO features": "프로 기능", + "Sync 9 more saves": "추가 9개 저장 파일 동기화", "Unlimited master nodes": "마스터 노드 무제한", "Unlimited parallel lines": "평행선 무제한", + "Random station names": "무작위 역 이름", "Expires at:": "만료일:", "Not applicable": "해당 없음", "Renew": "갱신", - "Redeem your subscription": "구독을 교환하세요", - "CDKey could be purchased in the following sites:": "CDKey는 다음 사이트에서 구매할 수 있습니다:", - "Enter your CDKey here": "여기에 CDKey를 입력하세요", + "Renew now and get an extra 45 days!": "지금 갱신하고 추가 45일을 받으세요!", + "Redeem your subscription": "구독 교환", + "CDKey could be purchased in the following sites:": "CD키는 다음 사이트에서 구매할 수 있습니다:", + "Enter your CDKey here": "여기에 CD키를 입력하세요", - "Oops! It seems there's a conflict": "이런! 충돌이 발생한 것 같습니다", - "The local save is newer than the cloud one. Which one would you like to keep?": "로컬 저장이 클라우드 저장보다 최신입니다. 어느 것을 유지하시겠습니까?", - "Local save": "로컬 저장", - "Replace cloud with local": "클라우드를 로컬로 교체", - "Download Local save": "로컬 저장 다운로드", - "Replace local with cloud": "로컬을 클라우드로 교체", - "Download Cloud save": "클라우드 저장 다운로드" + "Oops! It seems there's a conflict": "おっと!競合が発生したようです (おっと!きょうごうが はっせいした ようです)", + "The local save is newer than the cloud one. Which one would you like to keep?": "로컬 저장 파일이 클라우드 저장 파일보다 최신입니다. 어떤 버전을 유지하시겠습니까?", + "Local save": "로컬 저장 파일", + "Replace cloud with local": "클라우드를 로컬 저장 파일로 교체", + "Download Local save": "로컬 저장 파일 다운로드", + "Replace local with cloud": "로컬을 클라우드 저장 파일로 교체", + "Download Cloud save": "클라우드 저장 파일 다운로드" } diff --git a/src/i18n/translations/zh-Hans.json b/src/i18n/translations/zh-Hans.json index 34abfc8..7026e88 100644 --- a/src/i18n/translations/zh-Hans.json +++ b/src/i18n/translations/zh-Hans.json @@ -99,7 +99,7 @@ "Your password has been reset successfully.": "已成功重置您的密码。", "You're currently viewing a testing environment.": "您正在浏览测试环境。", - "Login status expired":"登录过期", + "Login status expired.":"登录过期。", "Log in / Sign up":"登录 / 注册", "Log out":"登出", "Change password":"更改密码", @@ -142,14 +142,18 @@ "Sync this slot":"同步此存档", "All subscriptions":"所有订阅", "Redeem":"兑换", + "RMP_CLOUD": "地铁线路图绘制器・天枢", + "RMP_EXPORT": "地铁线路图绘制器・玉衡", "With this subscription, the following features are unlocked:":"订阅后以下功能将会解锁", "PRO features":"专业功能", "Sync 9 more saves":"同步额外9个存档", "Unlimited master nodes":"大师节点无数量限制", "Unlimited parallel lines":"平行线段无数量限制", + "Random station names":"随机车站名称", "Expires at:":"过期于:", "Not applicable":"不适用", "Renew":"续期", + "Renew now and get an extra 45 days!": "现在续期并获得额外45天!", "Redeem your subscription":"兑换您的订阅", "CDKey could be purchased in the following sites:":"兑换码可在以下网站获取:", "Enter your CDKey here":"输入您的兑换码", diff --git a/src/i18n/translations/zh-Hant.json b/src/i18n/translations/zh-Hant.json index 411798b..4b83a86 100644 --- a/src/i18n/translations/zh-Hant.json +++ b/src/i18n/translations/zh-Hant.json @@ -99,7 +99,7 @@ "Your password has been reset successfully.": "已成功重設你的密碼。", "You're currently viewing a testing environment.": "你正在檢視測試環境。", - "Login status expired": "登入狀態已過期", + "Login status expired.": "登入狀態已過期。", "Log in / Sign up": "登入 / 註冊", "Log out": "登出", "Change password": "更改密碼", @@ -142,14 +142,18 @@ "Sync this slot": "同步此存檔", "All subscriptions": "所有訂閱", "Redeem": "兌換", + "RMP_CLOUD": "地鐵綫路圖繪製器・天樞", + "RMP_EXPORT": "地鐵綫路圖繪製器・玉衡", "With this subscription, the following features are unlocked:": "訂閱後將解鎖以下功能:", "PRO features": "專業功能", "Sync 9 more saves": "同步額外9個存檔", "Unlimited master nodes": "無限大師節點", "Unlimited parallel lines": "無限平行線段", + "Random station names": "隨機車站名稱", "Expires at:": "過期於:", "Not applicable": "不適用", "Renew": "續期", + "Renew now and get an extra 45 days!": "現在續期並獲得額外45天!", "Redeem your subscription": "兌換您的訂閱", "CDKey could be purchased in the following sites:": "CDKey可在以下網站購買:", "Enter your CDKey here": "在此輸入您的CDKey", diff --git a/src/index.tsx b/src/index.tsx index 22b1713..4db1d58 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,7 +7,7 @@ import i18n from './i18n/config'; import './index.css'; import './inject-seo'; import store from './redux'; -import { syncAfterLogin } from './redux/account/account-slice'; +import { fetchSubscription, syncAfterLogin } from './redux/account/account-slice'; import { addRemoteFont, closeApp, isShowDevtools, openApp, updateTabMetadata } from './redux/app/app-slice'; import initStore from './redux/init'; import { checkTokenAndRefreshStore } from './util/api'; @@ -46,6 +46,8 @@ rmgRuntime.ready().then(async () => { // Otherwise this is a no-op. await checkTokenAndRefreshStore(store); await store.dispatch(syncAfterLogin()); + // Fetch subscription info only if user is logged in. + store.dispatch(fetchSubscription()); renderApp(); diff --git a/src/redux/account/account-slice.ts b/src/redux/account/account-slice.ts index 8ca56e9..bc49ed8 100644 --- a/src/redux/account/account-slice.ts +++ b/src/redux/account/account-slice.ts @@ -1,19 +1,29 @@ import { logger } from '@railmapgen/rmg-runtime'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { RootState } from '../index'; -import { API_ENDPOINT, APILoginResponse, APISaveInfo, APISaveList, SAVE_KEY } from '../../util/constants'; -import { getRMPSave, notifyRMPSaveChange, setRMPSave } from '../../util/local-storage-save'; +import i18n from '../../i18n/config'; import { apiFetch } from '../../util/api'; +import { + API_ENDPOINT, + APILoginResponse, + APISaveInfo, + APISaveList, + APISubscription, + SAVE_KEY, +} from '../../util/constants'; +import { getRMPSave, notifyRMPSaveChange, setRMPSave } from '../../util/local-storage-save'; +import { RootState } from '../index'; +import { addNotification } from '../notification/notification-slice'; import { setLastChangedAtTimeStamp, setResolveConflictModal } from '../rmp-save/rmp-save-slice'; +type DateTimeString = `${string}T${string}Z`; export interface ActiveSubscriptions { - RMP_CLOUD: boolean; - RMP_EXPORT: boolean; + RMP_CLOUD?: DateTimeString; + RMP_EXPORT?: DateTimeString; } export const defaultActiveSubscriptions: ActiveSubscriptions = { - RMP_CLOUD: false, - RMP_EXPORT: false, + RMP_CLOUD: undefined, + RMP_EXPORT: undefined, }; export interface AccountState { @@ -91,7 +101,11 @@ export const fetchLogin = createAsyncThunk<{ error?: string; username?: string } }, } = (await loginRes.json()) as APILoginResponse; dispatch(login({ id: userId, name: username, email, token, expires, refreshToken, refreshExpires })); - await dispatch(fetchSaveList()); // make sure saves are set before syncAfterLogin + + dispatch(fetchSubscription()); + + // make sure saves are set before syncAfterLogin + await dispatch(fetchSaveList()); await dispatch(syncAfterLogin()); @@ -152,6 +166,47 @@ export const syncAfterLogin = createAsyncThunk( } ); +export const fetchSubscription = createAsyncThunk( + 'account/fetchSubscription', + async (_, { getState, dispatch, rejectWithValue }) => { + const { isLoggedIn, token } = (getState() as RootState).account; + if (!isLoggedIn || !token) return rejectWithValue('No token.'); + const rep = await apiFetch(API_ENDPOINT.SUBSCRIPTION, {}, token); + if (rep.status === 401) { + dispatch( + addNotification({ + title: i18n.t('Unable to retrieve your subscriptions'), + message: i18n.t('Login status expired.'), + duration: 60, + type: 'error', + }) + ); + dispatch(logout()); + return rejectWithValue('Login status expired.'); + } + if (rep.status !== 200) { + dispatch( + addNotification({ + title: i18n.t('Unable to retrieve your subscriptions'), + message: await rep.text(), + duration: 60, + type: 'error', + }) + ); + return rejectWithValue(await rep.text()); + } + const subscriptions = (await rep.json()).subscriptions as APISubscription[]; + const activeSubscriptions = structuredClone(defaultActiveSubscriptions); + for (const subscription of subscriptions) { + const type = subscription.type; + if (type in activeSubscriptions) { + activeSubscriptions[type as keyof ActiveSubscriptions] = subscription.expires as DateTimeString; + } + } + dispatch(setActiveSubscriptions(activeSubscriptions)); + } +); + const accountSlice = createSlice({ name: 'account', initialState, diff --git a/src/redux/notification/notification-slice.ts b/src/redux/notification/notification-slice.ts index 26699ed..f86b208 100644 --- a/src/redux/notification/notification-slice.ts +++ b/src/redux/notification/notification-slice.ts @@ -1,5 +1,5 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import rmgRuntime, { RMNotification } from '@railmapgen/rmg-runtime'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; export interface NotificationState { notifications: RMNotification[]; diff --git a/src/util/constants.ts b/src/util/constants.ts index 5bf32a6..b85533f 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -73,6 +73,11 @@ export interface APISaveList { currentSaveId: number; } +export interface APISubscription { + type: 'RMP' | 'RMP_CLOUD' | 'RMP_EXPORT'; + expires: string; +} + export enum SAVE_KEY { RMP = 'rmp__param', }