42 changed files with 1538 additions and 278 deletions
-
1.env
-
5package.json
-
3public/index.html
-
80src/api/index.ts
-
6src/api/service.ts
-
26src/assets/iconfont/iconfont.css
-
2src/assets/iconfont/iconfont.js
-
35src/assets/iconfont/iconfont.json
-
BINsrc/assets/iconfont/iconfont.ttf
-
BINsrc/assets/iconfont/iconfont.woff
-
BINsrc/assets/iconfont/iconfont.woff2
-
BINsrc/assets/share.png
-
46src/components/CoinPicker.tsx
-
51src/components/Modal.tsx
-
23src/components/ProductItem.tsx
-
37src/hooks/useWs.ts
-
4src/index.tsx
-
42src/pages/home/index.tsx
-
13src/pages/personal/AccountAssetsCard.tsx
-
30src/pages/personal/index.tsx
-
3src/pages/product/index.tsx
-
79src/pages/recharge/index.tsx
-
127src/pages/record/index.tsx
-
191src/pages/share/index.tsx
-
252src/pages/team/index.tsx
-
64src/pages/withdraw/index.tsx
-
131src/router/layout/index.tsx
-
123src/router/layout/ui.tsx
-
10src/router/routes.tsx
-
24src/store/index.ts
-
19src/styles/components.scss
-
27src/styles/layout.scss
-
89src/styles/personal.scss
-
3src/styles/recharge.scss
-
20src/styles/share.scss
-
2src/styles/theme.scss
-
14src/types/api.d.ts
-
49src/types/store.d.ts
-
2src/utils/copy.ts
-
33src/utils/index.ts
-
1src/utils/sign/sign.ts
-
109yarn.lock
@ -1,5 +1,6 @@ |
|||||
SKIP_PREFLIGHT_CHECK=true |
SKIP_PREFLIGHT_CHECK=true |
||||
GENERATE_SOURCEMAP=false |
GENERATE_SOURCEMAP=false |
||||
REACT_APP_BASE_URL='http://14.29.101.215:30307' |
REACT_APP_BASE_URL='http://14.29.101.215:30307' |
||||
|
REACT_APP_WS_URL='ws://14.29.101.215:30307' |
||||
REACT_APP_SHARE_LINK='http://14.29.101.215:30305/#/' |
REACT_APP_SHARE_LINK='http://14.29.101.215:30305/#/' |
||||
REACT_APP_SIGN_KEY='9527nft9527_@fsdgfsx' |
REACT_APP_SIGN_KEY='9527nft9527_@fsdgfsx' |
2
src/assets/iconfont/iconfont.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
After Width: 444 | Height: 465 | Size: 11 KiB |
@ -0,0 +1,46 @@ |
|||||
|
import { useMemo } from "react"; |
||||
|
import { Picker } from "react-vant"; |
||||
|
import { CoinList } from "~/types/store"; |
||||
|
|
||||
|
interface CoinPickerProps { |
||||
|
setIndex: Function; |
||||
|
list: CoinList[]; |
||||
|
index: number |
||||
|
} |
||||
|
|
||||
|
const CoinPicker = ({ setIndex, list, index }: CoinPickerProps) => { |
||||
|
|
||||
|
const coinList = useMemo(() => list.map(item => item.symbol), [list]); |
||||
|
|
||||
|
return ( |
||||
|
<Picker |
||||
|
columns={coinList} |
||||
|
popup={{ |
||||
|
round: true |
||||
|
}} |
||||
|
onConfirm={(_val: string, _item: Object, _index: number) => { |
||||
|
setIndex(_index); |
||||
|
}} |
||||
|
placeholder="" |
||||
|
value={coinList[index]} |
||||
|
> |
||||
|
{(_val, _, _action) => ( |
||||
|
<div onClick={() => _action.open()} style={{ width: '100%' }}> |
||||
|
{ |
||||
|
list[index] && ( |
||||
|
<div className='row-between'> |
||||
|
<div className='row-items'> |
||||
|
<img src={require(`~/assets/${list[index].symbol}.png`)} alt="" className="img-20" /> |
||||
|
<div className='ml-1 mt-3px fz-wb-550'>{list[index].symbol}</div> |
||||
|
</div> |
||||
|
<div className='iconfont icon-arrow-right-bold fz-20 fz-wb-550'></div> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
</div> |
||||
|
)} |
||||
|
</Picker> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default CoinPicker; |
@ -0,0 +1,51 @@ |
|||||
|
import { Overlay } from 'react-vant'; |
||||
|
import '~/styles/components.scss' |
||||
|
|
||||
|
interface ModalProps { |
||||
|
buttonText: string; |
||||
|
title: string; |
||||
|
children: JSX.Element; |
||||
|
buttonClick: Function; |
||||
|
visible: boolean; |
||||
|
setVisible: Function; |
||||
|
hiddenCloseIcon?: boolean; |
||||
|
showCancelButton?: boolean; |
||||
|
showCancelButtonText?: string; |
||||
|
showCancelButtonClick?: Function; |
||||
|
} |
||||
|
|
||||
|
const Modal = ( |
||||
|
{ title, buttonClick, visible, setVisible, children, buttonText, hiddenCloseIcon, showCancelButton, showCancelButtonText, showCancelButtonClick }: ModalProps |
||||
|
) => { |
||||
|
return ( |
||||
|
<Overlay |
||||
|
visible={visible} |
||||
|
className='row-center' |
||||
|
style={{ height: '100%' }} |
||||
|
> |
||||
|
<div className='modal-content'> |
||||
|
<div className='text-center row-center'> |
||||
|
<div style={{ flex: 1 }}></div> |
||||
|
<div style={{ flex: 1, whiteSpace: 'nowrap' }} className='fz-20 fz-wb-550'>{title}</div> |
||||
|
<div style={{ flex: 1 }} className="tae"> |
||||
|
{ |
||||
|
!hiddenCloseIcon && ( |
||||
|
<i className='iconfont icon-guanbi2 fz-24 fz-wb-550' onClick={() => setVisible(false)} /> |
||||
|
) |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
{children} |
||||
|
<div className='mt-3 row-center'> |
||||
|
<div className='modal-button row-center' onClick={() => buttonClick()}>{buttonText}</div> |
||||
|
{ |
||||
|
showCancelButton && |
||||
|
<div className='modal-button row-center ml-2' onClick={() => showCancelButtonClick && showCancelButtonClick()}>{showCancelButtonText || '關閉'}</div> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
</Overlay> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default Modal |
@ -0,0 +1,37 @@ |
|||||
|
import { useRef } from "react"; |
||||
|
import signGenerator from "~/utils/sign/sign"; |
||||
|
|
||||
|
const useWs = (path: string) => { |
||||
|
const baseUrl = `ws://14.29.101.215:30307/api/v1/${path}`; |
||||
|
// const baseUrl = `ws://192.168.124.52:8083/api/v1/${path}`;
|
||||
|
const ws = useRef<any>(null); |
||||
|
|
||||
|
const connect = (token: string) => { |
||||
|
let timestamp = Date.now(); |
||||
|
let signData = { |
||||
|
uri: `/api/v1/${path}`, |
||||
|
timestamp: timestamp, |
||||
|
args: "", |
||||
|
}; |
||||
|
const sign = signGenerator(signData); |
||||
|
ws.current = new WebSocket( |
||||
|
`${baseUrl}?Token=${token}&sign=${sign}×tamp=${timestamp}` |
||||
|
); |
||||
|
|
||||
|
ws.current.onMessage = (data: any) => { |
||||
|
console.log(data); |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
const disconnect = () => { |
||||
|
ws.current && ws.current.close(); |
||||
|
ws.current = null; |
||||
|
}; |
||||
|
|
||||
|
return { |
||||
|
connect, |
||||
|
disconnect, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
export default useWs; |
@ -0,0 +1,252 @@ |
|||||
|
import '~/styles/personal.scss' |
||||
|
import { Button, Divider, List, Overlay, Swiper, SwiperInstance, Toast } from "react-vant" |
||||
|
import BackBar from "~/components/BackBar" |
||||
|
import { useEffect, useRef, useState } from 'react' |
||||
|
import { getTime, splitAddress } from '~/utils' |
||||
|
import { observer } from 'mobx-react' |
||||
|
import store from '~/store' |
||||
|
import { my_invite, team_info } from '~/api' |
||||
|
import useCopyLink from '~/hooks/useCopy' |
||||
|
import { InviteRecordData } from '~/types/store' |
||||
|
import { useRouter } from '~/hooks/useRouter' |
||||
|
import { copy } from '~/utils/copy' |
||||
|
import QRCode from 'qrcode' |
||||
|
|
||||
|
const Team = () => { |
||||
|
|
||||
|
const { push } = useRouter() |
||||
|
const { token } = store.state |
||||
|
const { copyVal } = useCopyLink() |
||||
|
const [address, setAddress] = useState('') |
||||
|
const [inviteList, setInviteList] = useState([] as InviteRecordData[]) |
||||
|
const [finished, setFinished] = useState(true); |
||||
|
const [visible, setVisible] = useState(false); |
||||
|
const [cardState, setCardState] = useState([ |
||||
|
[ |
||||
|
{ title: '我的推薦人', value: '000000000', id: 1 }, |
||||
|
{ title: '級別獎勵', value: '10% 5%', id: 2 }, |
||||
|
], |
||||
|
[ |
||||
|
{ title: '獎勵金額', value: '0U', id: 4 }, |
||||
|
{ title: '直推人數', value: '0人', id: 6 }, |
||||
|
{ title: '閒推人數', value: '0人', id: 5 }, |
||||
|
], |
||||
|
]) |
||||
|
|
||||
|
const query = useRef({ page: 1, page_size: 20 }) |
||||
|
|
||||
|
const getInviteData = async () => { |
||||
|
let res: any = await my_invite(query.current); |
||||
|
if (res && res.code === 0 && res.data) { |
||||
|
if (res.data.length < 20) { |
||||
|
if (inviteList.length <= 0) { |
||||
|
setInviteList(res.data) |
||||
|
} else { |
||||
|
setInviteList([...inviteList, ...res.data]) |
||||
|
} |
||||
|
setFinished(true) |
||||
|
return |
||||
|
} |
||||
|
query.current.page = query.current.page + 1 |
||||
|
if (inviteList.length <= 0) { |
||||
|
setInviteList(res.data) |
||||
|
} else { |
||||
|
setInviteList([...inviteList, ...res.data]) |
||||
|
}; |
||||
|
setFinished(false) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
useEffect(() => { |
||||
|
const getData = async () => { |
||||
|
const res: any = await team_info() |
||||
|
if (res && res.code === 0) { |
||||
|
cardState[0][0].value = res.data.inviti_address || '000000000000000' |
||||
|
cardState[1][0].value = res.data.award ? res.data.award + "U" : "0U" |
||||
|
cardState[1][1].value = res.data.direct_count ? res.data.direct_count + '人' : '0人' |
||||
|
cardState[1][2].value = res.data.indirect_count ? res.data.indirect_count + '人' : '0人' |
||||
|
setCardState([...cardState]) |
||||
|
setAddress(res.data.address) |
||||
|
} |
||||
|
console.log(res); |
||||
|
} |
||||
|
|
||||
|
token && getData() |
||||
|
token && getInviteData(); |
||||
|
!token && push('/', null, true) |
||||
|
}, [token]) |
||||
|
|
||||
|
return ( |
||||
|
<div className="plr-2 team"> |
||||
|
<BackBar |
||||
|
title='团队' |
||||
|
/> |
||||
|
<Button className="mt-2 button" onClick={() => setVisible(true)}>分享赚取佣金</Button> |
||||
|
<div className="row-between" style={{ alignItems: 'flex-end' }}> |
||||
|
<div className='fz-24 fz-wb-550 mb-6px'>邀请好友,挣奖励</div> |
||||
|
<div className="tar"> |
||||
|
<div className="fz-wb-550">最高可得</div> |
||||
|
<div className="fz-wb-550 fz-60">10%</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div className='card-box'> |
||||
|
{ |
||||
|
cardState.map((v, index) => ( |
||||
|
<div key={index} className="row-between" style={{ justifyContent: 'space-around' }}> |
||||
|
{ |
||||
|
v.map(item => ( |
||||
|
<div key={item.id} className={`tac ${index === 1 && 'mt-2'}`}> |
||||
|
<div className='fz-14'>{item.title}</div> |
||||
|
<div className='mt-1 fz-wb-550'> |
||||
|
{item.id === 1 ? splitAddress(item.value) : item.value} |
||||
|
{ |
||||
|
item.id === 1 && ( |
||||
|
<i className='iconfont icon-fuzhi_copy white ml-5px' onClick={() => copyVal(item.value)}></i> |
||||
|
) |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
)) |
||||
|
} |
||||
|
</div> |
||||
|
)) |
||||
|
} |
||||
|
</div> |
||||
|
|
||||
|
<div className='mt-3'> |
||||
|
<div className='fz-18'>我的邀请</div> |
||||
|
<div className='mt-5px'> |
||||
|
<Divider style={{ margin: 0, borderColor: '#2A2C24' }} /> |
||||
|
</div> |
||||
|
<div className='row mt-1 tac fz-14'> |
||||
|
<div style={{ flex: 1 }}>地址</div> |
||||
|
<div style={{ flex: 1 }}></div> |
||||
|
<div style={{ flex: 1 }}>時間</div> |
||||
|
</div> |
||||
|
<List |
||||
|
finished={finished} |
||||
|
onLoad={getInviteData} |
||||
|
errorText="請求失敗,點擊重新加載" |
||||
|
finishedText="已經到底了" |
||||
|
offset={10} |
||||
|
> |
||||
|
<div> |
||||
|
{ |
||||
|
inviteList.map((item, index) => ( |
||||
|
<div className='row mt-2 tac fz-14' key={index}> |
||||
|
<div style={{ flex: 1 }}>{splitAddress(item.address)}</div> |
||||
|
<div style={{ flex: 1 }}></div> |
||||
|
<div style={{ flex: 1 }}>{getTime(item.time * 1000)}</div> |
||||
|
</div> |
||||
|
)) |
||||
|
} |
||||
|
</div> |
||||
|
</List> |
||||
|
</div> |
||||
|
{ |
||||
|
visible && <ShareModal visible={visible} setVisible={setVisible} address={address} /> |
||||
|
} |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const ShareModal = ( |
||||
|
{ visible, setVisible, address }: { visible: boolean, setVisible: Function, address: string } |
||||
|
) => { |
||||
|
|
||||
|
const [swiperIndex, setSwiperIndex] = useState(0) |
||||
|
const swiperRef = useRef<SwiperInstance>(null) |
||||
|
const [qrcodeUri, setQrcodeUri] = useState('') |
||||
|
|
||||
|
const handleSwiper = () => { |
||||
|
if (swiperIndex === 0) { |
||||
|
swiperRef.current?.swipeNext() |
||||
|
} else { |
||||
|
swiperRef.current?.swipePrev() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const downloadQrcode = () => { |
||||
|
if (!qrcodeUri) return |
||||
|
const link = document.createElement('a') |
||||
|
link.href = qrcodeUri |
||||
|
link.download = `${Date.now()}.png` |
||||
|
link.click() |
||||
|
link.remove() |
||||
|
} |
||||
|
|
||||
|
useEffect(() => { |
||||
|
copy(process.env.REACT_APP_SHARE_LINK + address) |
||||
|
QRCode.toDataURL(process.env.REACT_APP_SHARE_LINK + address, (err, url) => { |
||||
|
if (url) { |
||||
|
setQrcodeUri(url) |
||||
|
} |
||||
|
}) |
||||
|
}, []) |
||||
|
|
||||
|
return ( |
||||
|
<Overlay visible={visible}> |
||||
|
<div className='mt-5 white-color overlay'> |
||||
|
<div className='row-justify-end pr-3'> |
||||
|
<i className='iconfont icon-close fz-36' onClick={() => setVisible(false)}></i> |
||||
|
</div> |
||||
|
<Swiper |
||||
|
ref={swiperRef} |
||||
|
indicator={() => <></>} |
||||
|
onChange={(index) => setSwiperIndex(index)} |
||||
|
initialSwipe={swiperIndex} |
||||
|
touchable={false} |
||||
|
> |
||||
|
<Swiper.Item> |
||||
|
<div className='swiper-height'> |
||||
|
<div className='row-center'> |
||||
|
<div className='mt-5'> |
||||
|
<img src={require('~/assets/share.png')} alt="" className='share-img' /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div className='url mt-2 row-center'> |
||||
|
{splitAddress(process.env.REACT_APP_SHARE_LINK + address, 14)} |
||||
|
<i className='iconfont icon-fuzhi_copy ml-1 fz-24' onClick={() => { |
||||
|
copy(process.env.REACT_APP_SHARE_LINK + address, () => { |
||||
|
Toast.success('复制成功') |
||||
|
}) |
||||
|
}}></i> |
||||
|
</div> |
||||
|
<div className='mt-2 tac fz-wb-550 fz-26'>成功複製分享碼</div> |
||||
|
<div className='mt-2 tac'>轉發給好友並在錢包裏打開完成綁定</div> |
||||
|
</div> |
||||
|
</Swiper.Item> |
||||
|
<Swiper.Item> |
||||
|
<div className='swiper-height'> |
||||
|
<div className='row-center mt-5'> |
||||
|
<img src={qrcodeUri} alt="" className='share-img' /> |
||||
|
</div> |
||||
|
<div className='url mt-2 row-center'> |
||||
|
截圖或下載圖片 |
||||
|
<i className='iconfont icon-download ml-1 fz-24' onClick={downloadQrcode}></i> |
||||
|
</div> |
||||
|
<div className='mt-2 tac fz-wb-550 fz-26'>掃描綁定關係</div> |
||||
|
<div className='mt-2 tac'>在好友錢包裡掃一掃完成綁定</div> |
||||
|
</div> |
||||
|
</Swiper.Item> |
||||
|
</Swiper> |
||||
|
|
||||
|
<div className='row-center' > |
||||
|
<div className='button' onClick={handleSwiper}> |
||||
|
<div className='text-index plr-2'> |
||||
|
<div> |
||||
|
{swiperIndex === 0 ? '切換二維碼' : '切換分享鏈接'} |
||||
|
</div> |
||||
|
<i className='iconfont icon-arrow-right-bold fz-20'></i> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</Overlay> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
export default observer(Team) |
@ -0,0 +1,123 @@ |
|||||
|
import { ethers } from "ethers"; |
||||
|
import { useRef } from "react"; |
||||
|
import { Toast } from "react-vant"; |
||||
|
import Modal from "../../components/Modal"; |
||||
|
interface UIProps { |
||||
|
visible: boolean, |
||||
|
setVisible: Function, |
||||
|
onClick?: Function, |
||||
|
address?: string |
||||
|
} |
||||
|
|
||||
|
export const UnLogin = ({ visible, setVisible }: UIProps) => ( |
||||
|
<Modal |
||||
|
title="拒絕訪問" |
||||
|
buttonClick={setVisible} |
||||
|
setVisible={setVisible} |
||||
|
visible={visible} |
||||
|
buttonText="關閉" |
||||
|
> |
||||
|
<div> |
||||
|
<div className="mt-2 fz-14 tac fz-wb-550" style={{ color: '#FF5300' }}>訪問失敗</div> |
||||
|
<div className="unlogin-box row-center fz-14">未檢測到錢包,請登錄錢包後重新點擊</div> |
||||
|
</div> |
||||
|
</Modal> |
||||
|
) |
||||
|
|
||||
|
export const VaildLink = ({ visible, setVisible }: UIProps) => ( |
||||
|
<Modal |
||||
|
title="綁定推薦人" |
||||
|
buttonClick={() => setVisible(false)} |
||||
|
setVisible={() => setVisible(false)} |
||||
|
visible={visible} |
||||
|
buttonText="關閉" |
||||
|
> |
||||
|
<div> |
||||
|
<div className="mt-2 fz-14 tac fz-wb-550" style={{ color: '#FF5300' }}>綁定失敗</div> |
||||
|
<div className="unlogin-box row-center fz-14">無效的分享鏈接</div> |
||||
|
</div> |
||||
|
</Modal> |
||||
|
) |
||||
|
|
||||
|
export const DefaultBind = ({ visible, setVisible, onClick }: UIProps) => { |
||||
|
|
||||
|
const addressRefs = useRef<HTMLInputElement>(null) |
||||
|
|
||||
|
const confirm = () => { |
||||
|
let value = addressRefs.current?.value |
||||
|
if (!value) return Toast.info('請輸入推薦鏈接!') |
||||
|
let newValue = value?.split('/#/') |
||||
|
if (!ethers.utils.isAddress(newValue[1])) return Toast.info('無效的分享鏈接') |
||||
|
onClick && onClick(newValue[1]) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<Modal |
||||
|
visible={visible} |
||||
|
setVisible={setVisible} |
||||
|
title="綁定推薦人" |
||||
|
buttonClick={confirm} |
||||
|
buttonText="確認綁定" |
||||
|
> |
||||
|
<div> |
||||
|
<div className="tac mt-2 fz-14">推薦鏈接</div> |
||||
|
<input |
||||
|
type="text" |
||||
|
className="default-bind-input" |
||||
|
placeholder="請輸入推薦鏈接" |
||||
|
ref={addressRefs} |
||||
|
/> |
||||
|
</div> |
||||
|
</Modal> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export const BindRmd = ({ visible, setVisible, address, onClick }: UIProps) => ( |
||||
|
<Modal |
||||
|
visible={visible} |
||||
|
setVisible={() => setVisible(false)} |
||||
|
title="綁定推薦人" |
||||
|
buttonClick={() => onClick && onClick(address)} |
||||
|
buttonText="確認綁定" |
||||
|
hiddenCloseIcon |
||||
|
> |
||||
|
<div> |
||||
|
<div className="tac mt-2 fz-14">推薦人地址</div> |
||||
|
<div className="default-bind-input row-center">{address}</div> |
||||
|
</div> |
||||
|
</Modal> |
||||
|
) |
||||
|
|
||||
|
export const BindSuccess = ({ visible, setVisible, address }: UIProps) => ( |
||||
|
<Modal |
||||
|
visible={visible} |
||||
|
setVisible={setVisible} |
||||
|
title="綁定推薦人" |
||||
|
buttonClick={setVisible} |
||||
|
buttonText="關閉" |
||||
|
hiddenCloseIcon |
||||
|
> |
||||
|
<div> |
||||
|
<div className="tac mt-2 fz-14">綁定成功</div> |
||||
|
<div className="default-bind-input row-center" style={{ color: '#1BA27A' }}>{address}</div> |
||||
|
</div> |
||||
|
</Modal> |
||||
|
) |
||||
|
|
||||
|
|
||||
|
export const AlreadyBind = ({ visible, setVisible, onClick }: UIProps) => ( |
||||
|
<Modal |
||||
|
title="綁定推薦人" |
||||
|
buttonClick={() => onClick && onClick()} |
||||
|
setVisible={() => setVisible(false)} |
||||
|
visible={visible} |
||||
|
buttonText='查看綁定人' |
||||
|
showCancelButton |
||||
|
showCancelButtonClick={() => setVisible(false)} |
||||
|
> |
||||
|
<div> |
||||
|
<div className="mt-2 fz-14 tac fz-wb-550" style={{ color: '#FF5300' }}>綁定失敗</div> |
||||
|
<div className="unlogin-box row-center fz-14">該錢包已有推薦人,不可重複綁定推薦人</div> |
||||
|
</div> |
||||
|
</Modal> |
||||
|
) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue