Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
412 changes: 20 additions & 392 deletions src/components/menu/account-view/saves-section.tsx

Large diffs are not rendered by default.

222 changes: 222 additions & 0 deletions src/components/menu/account-view/share-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import {
ActionIcon,
Alert,
Button,
CopyButton,
Group,
Modal,
Space,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MdCheck, MdContentCopy, MdWarning } from 'react-icons/md';
import { useRootDispatch, useRootSelector } from '../../../redux';
import { fetchSaveList, logout } from '../../../redux/account/account-slice';
import { addNotification } from '../../../redux/notification/notification-slice';
import { apiFetch } from '../../../util/api';
import { API_ENDPOINT, APISaveInfo } from '../../../util/constants';

const defaultSaveInfo: APISaveInfo = {
index: '0',
id: 0,
hash: '',
lastUpdateAt: '',
};

interface ShareModalProps {
opened: boolean;
onClose: () => void;
shareSaveInfo?: APISaveInfo;
}

const ShareModal: React.FC<ShareModalProps> = ({ opened, onClose, shareSaveInfo }) => {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const dispatch = useRootDispatch();
const { id: saveID, index, share } = shareSaveInfo ?? defaultSaveInfo;

const { token, activeSubscriptions } = useRootSelector(state => state.account);

const handleCopy = () => {
setCopied(true);
setTimeout(() => setCopied(false), 2000); // Reset copied status after 2 seconds
};

const handleCreateShare = async () => {
try {
const rep = await apiFetch(API_ENDPOINT.SHARE + `/${saveID}`, { method: 'POST' }, token);
if (rep.status === 401) {
dispatch(
addNotification({
type: 'error',
title: t('Session expired.'),
message: t('Please log in again.'),
duration: 5000,
})
);
dispatch(logout());
return;
}
if (rep.status !== 201) {
dispatch(
addNotification({
type: 'error',
title: t('Error'),
message: await rep.text(),
duration: 5000,
})
);
return;
}
dispatch(fetchSaveList());
dispatch(
addNotification({
type: 'success',
title: t('Success'),
message: t('Share link created successfully.'),
duration: 3000,
})
);
} catch (e) {
dispatch(
addNotification({
type: 'error',
title: t('Error'),
message: (e as Error).message,
duration: 5000,
})
);
}
};

const handleDeleteShare = async () => {
try {
const rep = await apiFetch(API_ENDPOINT.SHARE + `/${saveID}`, { method: 'DELETE' }, token);
if (rep.status === 401) {
dispatch(
addNotification({
type: 'error',
title: t('Session expired.'),
message: t('Please log in again.'),
duration: 5000,
})
);
dispatch(logout());
return;
}
if (rep.status !== 204) {
dispatch(
addNotification({
type: 'error',
title: t('Error'),
message: await rep.text(),
duration: 5000,
})
);
return;
}
dispatch(fetchSaveList());
dispatch(
addNotification({
type: 'success',
title: t('Success'),
message: t('Share link deleted successfully.'),
duration: 3000,
})
);
} catch (e) {
dispatch(
addNotification({
type: 'error',
title: t('Error'),
message: (e as Error).message,
duration: 5000,
})
);
}
};

return (
<Modal
opened={opened}
onClose={onClose}
title={<Title size="h3">{`${t('Share work {{index}}', { index })}`}</Title>}
size="lg"
>
<Text mb="sm">{t('Share this link with others to allow them to view this work.')}</Text>
{share ? (
<Stack>
<TextInput
value={`${window.origin}?app=rmp&searchParams=${share.s}.org`}
readOnly
style={{ flexGrow: 1 }}
rightSection={
<CopyButton value={`${window.origin}?app=rmp&searchParams=${share.s}.org`} timeout={2000}>
{({ copied: btnCopied, copy }) => (
<Tooltip label={btnCopied ? t('Copied') : t('Copy')} withArrow position="right">
<ActionIcon
color={btnCopied ? 'teal' : 'gray'}
onClick={() => {
copy();
handleCopy();
}}
>
{btnCopied || copied ? <MdCheck /> : <MdContentCopy />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
}
/>
<Group justify="space-between">
<Text>
{t('This link is valid until:')}{' '}
{new Date(
share.validUntil ?? activeSubscriptions.RMP_EXPORT ?? Date.now().toLocaleString()
).toLocaleString()}
</Text>
<Button color="red" onClick={handleDeleteShare} ml="xs" size="sm" variant="light">
{t('Delete Share')}
</Button>
</Group>
</Stack>
) : (
<Group>
<Button onClick={handleCreateShare} leftSection={<MdContentCopy />}>
{t('Create Share Link')}
</Button>
</Group>
)}

<Space h="md" />
<Alert
color="yellow"
icon={<MdWarning />}
title={t(
'Please be aware that sharing this link does not create a separate copy of your work. Instead, everyone who opens the link will always see the most up-to-date version. So, if you make changes after sharing the link, those changes will be visible to everyone who has the link.'
)}
/>
<Space h="md" />
<Alert
color="yellow"
icon={<MdWarning />}
title={t(
'Link will be valid as long as you have an active subscription. Otherwise, the link will be invalid after 1 day.'
)}
/>

<Group mt="md">
<Button onClick={onClose} variant="light" style={{ marginLeft: 'auto' }}>
{t('Close')}
</Button>
</Group>
</Modal>
);
};

export default ShareModal;
20 changes: 19 additions & 1 deletion src/i18n/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
"text4": "ご不明な場合、または当社の開発者の指示なしに操作している場合は、このダイアログを閉じてください。"
},

"Session expired.": "セッションの有効期限が切れました。",
"Please log in again.": "再度ログインしてください。",
"Error": "失敗",
"Success": "成功",
"Close": "閉じる",

"About": "概要",
"Allow cookies to help improve our website": "Cookie を許可して網頁の改善にご協力ください",
"Appearance": "外観",
Expand Down Expand Up @@ -164,5 +170,17 @@
"Replace cloud with local": "雲を本地で置き換える",
"Download Local save": "本地保存をダウンロード",
"Replace local with cloud": "本地を雲で置き換える",
"Download Cloud save": "雲保存をダウンロード"
"Download Cloud save": "雲保存をダウンロード",

"Share work {{index}}": "作品 {{index}} を共有",
"Share this link with others to allow them to view this work.": "このリンクを他の方と共有し、作品の閲覧を許可してください。",
"Copied": "コピーしました",
"Copy": "コピー",
"Share link created successfully.": "共有リンクが正常に作成されました。",
"Share link deleted successfully.": "共有リンクが正常に削除されました。",
"This link is valid until:": "このリンクの有効期限:",
"Delete Share": "共有を削除",
"Create Share Link": "共有リンクを作成",
"Please be aware that sharing this link does not create a separate copy of your work. Instead, everyone who opens the link will always see the most up-to-date version. So, if you make changes after sharing the link, those changes will be visible to everyone who has the link.": "ご注意:このリンクを共有しても、作品の個別のコピーは作成されません。代わりに、リンクを開いた人は常に最新バージョンを閲覧することになります。そのため、リンク共有後に変更を加えた場合、その変更はリンクをお持ちのすべての方に表示されます。",
"Link will be valid as long as you have an active subscription. Otherwise, the link will be invalid after 1 day.": "有効なご購読がある限り、リンクは有効です。それ以外の場合、リンクは 1 日後に無効になります。"
}
22 changes: 20 additions & 2 deletions src/i18n/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
"text4": "확실하지 않거나 당사 개발자의 안내 없이 작업하는 경우 이 대화 상자를 닫으십시오."
},

"Session expired.": "세션이 만료되었습니다.",
"Please log in again.": "다시 로그인해주세요.",
"Error": "오류",
"Success": "성공",
"Close": "닫기",

"About": "정보",
"Allow cookies to help improve our website": "쿠키를 허용하여 웹사이트 개선에 도움을 주세요",
"Appearance": "모양",
Expand Down Expand Up @@ -158,11 +164,23 @@
"CDKey could be purchased in the following sites:": "CD키는 다음 사이트에서 구매할 수 있습니다:",
"Enter your CDKey here": "여기에 CD키를 입력하세요",

"Oops! It seems there's a conflict": "おっと!競合が発生したようです (おっと!きょうごうが はっせいした ようです)",
"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": "클라우드 저장 파일 다운로드"
"Download Cloud save": "클라우드 저장 파일 다운로드",

"Share work {{index}}": "작업 공유 {{index}}",
"Share this link with others to allow them to view this work.": "이 링크를 다른 사람과 공유하여 작업을 볼 수 있도록 하세요.",
"Copied": "복사됨",
"Copy": "복사",
"Share link created successfully.": "공유 링크가 성공적으로 만들어졌습니다.",
"Share link deleted successfully.": "공유 링크가 성공적으로 삭제되었습니다.",
"This link is valid until:": "이 링크는 다음 날짜까지 유효합니다:",
"Delete Share": "공유 삭제",
"Create Share Link": "공유 링크 만들기",
"Please be aware that sharing this link does not create a separate copy of your work. Instead, everyone who opens the link will always see the most up-to-date version. So, if you make changes after sharing the link, those changes will be visible to everyone who has the link.": "이 링크를 공유해도 작업의 별도 사본이 만들어지지 않습니다. 대신 링크를 여는 모든 사람은 항상 최신 버전을 보게 됩니다. 따라서 링크를 공유한 후 변경하면 해당 변경 사항이 링크를 가진 모든 사람에게 표시됩니다.",
"Link will be valid as long as you have an active subscription. Otherwise, the link will be invalid after 1 day.": "활성 구독이 있는 한 링크는 유효합니다. 그렇지 않으면 링크는 1일 후에 만료됩니다."
}
20 changes: 19 additions & 1 deletion src/i18n/translations/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
"text4": "如果您不确定,或者不是在我们的开发者的指引下操作,请关闭这个对话框。"
},

"Session expired.": "会话已过期。",
"Please log in again.": "请重新登录。",
"Error": "错误",
"Success": "成功",
"Close": "关闭",

"About": "关于",
"Allow cookies to help improve our website": "允许用 Cookies 帮助改进本网站",
"Appearance": "外观",
Expand Down Expand Up @@ -164,5 +170,17 @@
"Replace cloud with local": "用本地存档替换云端",
"Download Local save": "下载本地存档",
"Replace local with cloud": "用云端存档替换本地",
"Download Cloud save": "下载云端存档"
"Download Cloud save": "下载云端存档",

"Share work {{index}}": "分享作品 {{index}}",
"Share this link with others to allow them to view this work.": "分享此链接给他人以查看此作品。",
"Copied": "已复制",
"Copy": "复制",
"Share link created successfully.": "分享链接创建成功。",
"Share link deleted successfully.": "分享链接删除成功。",
"This link is valid until:": "此链接有效期至:",
"Delete Share": "删除分享",
"Create Share Link": "创建分享链接",
"Please be aware that sharing this link does not create a separate copy of your work. Instead, everyone who opens the link will always see the most up-to-date version. So, if you make changes after sharing the link, those changes will be visible to everyone who has the link.": "请注意,分享此链接不会创建您作品的副本。相反,每个打开链接的人都将始终看到最新版本。因此,如果您在分享链接后进行更改,这些更改将对拥有该链接的每个人可见。",
"Link will be valid as long as you have an active subscription. Otherwise, the link will be invalid after 1 day.": "只要您拥有有效订阅,链接便会一直有效。否则,链接将在创建后 1 天内失效。"
}
20 changes: 19 additions & 1 deletion src/i18n/translations/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
"text4": "若你未能確定,或不是在我們的開發人員的指引下操作,請關閉這個視窗。"
},

"Session expired.": "工作階段已過期。",
"Please log in again.": "請重新登入。",
"Error": "錯誤",
"Success": "成功",
"Close": "關閉",

"About": "關於",
"Allow cookies to help improve our website": "容許以 Cookies 幫助改進本網站",
"Appearance": "外觀",
Expand Down Expand Up @@ -164,5 +170,17 @@
"Replace cloud with local": "用本地存檔替換雲端",
"Download Local save": "下載本地存檔",
"Replace local with cloud": "用雲端存檔替換本地",
"Download Cloud save": "下載雲端存檔"
"Download Cloud save": "下載雲端存檔",

"Share work {{index}}": "分享作品 {{index}}",
"Share this link with others to allow them to view this work.": "分享此連結給他人以檢視此作品。",
"Copied": "已複製",
"Copy": "複製",
"Share link created successfully.": "分享連結建立成功。",
"Share link deleted successfully.": "分享連結刪除成功。",
"This link is valid until:": "此連結有效期至:",
"Delete Share": "刪除分享",
"Create Share Link": "建立分享連結",
"Please be aware that sharing this link does not create a separate copy of your work. Instead, everyone who opens the link will always see the most up-to-date version. So, if you make changes after sharing the link, those changes will be visible to everyone who has the link.": "請注意,分享此連結不會建立您作品的副本。相反,每個開啟連結的人都將始終看到最新版本。因此,如果您在分享連結後進行變更,這些變更將對擁有該連結的每個人可見。",
"Link will be valid as long as you have an active subscription. Otherwise, the link will be invalid after 1 day.": "只要您擁有有效訂閱,連結便會一直有效。否則,連結將在 1 天後失效。"
}
11 changes: 9 additions & 2 deletions src/util/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,28 @@ export enum API_ENDPOINT {
SUBSCRIPTION = '/subscription',
SUBSCRIPTION_REDEEM = '/subscription/redeem',
SAVES = '/saves',
SHARE = '/share',
}

export const API_URL = 'https://railmapgen.org/v1';
// export const API_URL = 'http://localhost:3000/v1';

type Timestamp = `${number}-${number}-${number}T${number}:${number}:${number}Z`;

export interface APILoginResponse {
user: { id: number; name: string };
tokens: { access: { token: string; expires: string }; refresh: { token: string; expires: string } };
tokens: { access: { token: string; expires: Timestamp }; refresh: { token: string; expires: Timestamp } };
}

export interface APISaveInfo {
index: string;
id: number;
hash: string;
lastUpdateAt: string;
lastUpdateAt: Timestamp;
share?: {
s: string;
validUntil: Timestamp | null;
};
}

export interface APISaveList {
Expand Down
Loading