diff --git a/.gitmodules b/.gitmodules index 59e10ee..c8bff63 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ [submodule "src/react-lazyload"] path = src/react-lazyload url = https://github.com/xmcp/react-lazyload - -[submodule "src/infrastructure"] - path = src/infrastructure - url = https://github.com/thuhole/infrastructure diff --git a/src/infrastructure b/src/infrastructure deleted file mode 160000 index e3c39ff..0000000 --- a/src/infrastructure +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e3c39ff4e3efaad45e16796d9eb511345d4cbcf4 diff --git a/src/infrastructure/appicon/course_survey.png b/src/infrastructure/appicon/course_survey.png new file mode 100644 index 0000000..a2ea2e3 Binary files /dev/null and b/src/infrastructure/appicon/course_survey.png differ diff --git a/src/infrastructure/appicon/dropdown.png b/src/infrastructure/appicon/dropdown.png new file mode 100644 index 0000000..0ac094a Binary files /dev/null and b/src/infrastructure/appicon/dropdown.png differ diff --git a/src/infrastructure/appicon/dropdown_rev.png b/src/infrastructure/appicon/dropdown_rev.png new file mode 100644 index 0000000..ad14288 Binary files /dev/null and b/src/infrastructure/appicon/dropdown_rev.png differ diff --git a/src/infrastructure/appicon/hole.png b/src/infrastructure/appicon/hole.png new file mode 100644 index 0000000..3089e7b Binary files /dev/null and b/src/infrastructure/appicon/hole.png differ diff --git a/src/infrastructure/appicon/homepage.png b/src/infrastructure/appicon/homepage.png new file mode 100644 index 0000000..c5cd092 Binary files /dev/null and b/src/infrastructure/appicon/homepage.png differ diff --git a/src/infrastructure/appicon/imasugu.png b/src/infrastructure/appicon/imasugu.png new file mode 100644 index 0000000..43c0bd7 Binary files /dev/null and b/src/infrastructure/appicon/imasugu.png differ diff --git a/src/infrastructure/appicon/imasugu_rev.png b/src/infrastructure/appicon/imasugu_rev.png new file mode 100644 index 0000000..dc2e505 Binary files /dev/null and b/src/infrastructure/appicon/imasugu_rev.png differ diff --git a/src/infrastructure/appicon/score.png b/src/infrastructure/appicon/score.png new file mode 100644 index 0000000..105a4c1 Binary files /dev/null and b/src/infrastructure/appicon/score.png differ diff --git a/src/infrastructure/appicon/syllabus.png b/src/infrastructure/appicon/syllabus.png new file mode 100644 index 0000000..4ea1557 Binary files /dev/null and b/src/infrastructure/appicon/syllabus.png differ diff --git a/src/infrastructure/const.js b/src/infrastructure/const.js new file mode 100644 index 0000000..b8bc1b1 --- /dev/null +++ b/src/infrastructure/const.js @@ -0,0 +1,2 @@ +// export const THUHOLE_API_ROOT='//localhost:5001/'; +export const THUHOLE_API_ROOT = 'https://thuhole.com/' diff --git a/src/infrastructure/elevator.js b/src/infrastructure/elevator.js new file mode 100644 index 0000000..0ee93a4 --- /dev/null +++ b/src/infrastructure/elevator.js @@ -0,0 +1,47 @@ +const DUMP_VER='dump_v1'; + +function dump() { + return JSON.stringify({ + _dump_ver: DUMP_VER, + token: localStorage['TOKEN']||null, + hole_config: localStorage['hole_config']||null, + }); +} +function load(s) { + console.log('elevator: loading',s); + let obj=JSON.parse(s); + if(obj._dump_ver!==DUMP_VER) { + console.error('elevator: loading version mismatch, current',DUMP_VER,'param',obj._dump_ver); + return; + } + if(localStorage['TOKEN']===undefined && obj.token) { + console.log('replace token'); + localStorage['TOKEN']=obj.token; + } + if(localStorage['hole_config']===undefined && obj.hole_config) { + console.log('replace hole config'); + localStorage['hole_config']=obj.hole_config; + } +} + +export function elevate() { + // load + // '?foo=fo&bar=ba' -> [["foo","fo"],["bar","ba"]] + let params=window.location.search.substr(1).split('&').map((kv)=>kv.split('=')); + params.forEach((kv)=>{ + if(kv.length===2 && kv[0]==='_elevator_data') { + load(decodeURIComponent(kv[1])); + let url=new URL(window.location.href); + url.search=''; + window.history.replaceState('','',url.href); + } + }); + + // dump + if(window.location.protocol==='http:' && window.location.hostname==='pkuhelper.pku.edu.cn') { + let url=new URL(window.location.href); + url.protocol='https:'; + url.search='?_elevator_data='+encodeURIComponent(dump()); + window.location.replace(url.href); + } +} \ No newline at end of file diff --git a/src/infrastructure/functions.js b/src/infrastructure/functions.js new file mode 100644 index 0000000..842ddaa --- /dev/null +++ b/src/infrastructure/functions.js @@ -0,0 +1,35 @@ +export function get_json(res) { + if(!res.ok) throw Error(`网络错误 ${res.status} ${res.statusText}`); + return ( + res + .text() + .then((t)=>{ + try { + return JSON.parse(t); + } catch(e) { + console.error('json parse error'); + console.trace(e); + console.log(t); + throw new SyntaxError('JSON Parse Error '+t.substr(0,50)); + } + }) + ); +} + +export function listen_darkmode(override) { // override: true/false/undefined + function update_color_scheme() { + if(override===undefined ? window.matchMedia('(prefers-color-scheme: dark)').matches : override) + document.body.classList.add('root-dark-mode'); + else + document.body.classList.remove('root-dark-mode'); + } + + update_color_scheme(); + window.matchMedia('(prefers-color-scheme: dark)').addListener(()=>{ + update_color_scheme(); + }); +} + +export function API_VERSION_PARAM() { + return '&PKUHelperAPI=3.0&jsapiver='+encodeURIComponent((process.env.REACT_APP_BUILD_INFO||'null')+'-'+(Math.floor(+new Date()/7200000)*2)); +} \ No newline at end of file diff --git a/src/infrastructure/global.css b/src/infrastructure/global.css new file mode 100644 index 0000000..6be140f --- /dev/null +++ b/src/infrastructure/global.css @@ -0,0 +1,37 @@ +:root { + --foreground-dark: hsl(0,0%,93%); +} + +body { + margin: 0; + padding: 0; + overflow-x: hidden; + text-size-adjust: 100%; +} + +body, textarea, pre { + font-family: 'Segoe UI', '微软雅黑', 'Microsoft YaHei', sans-serif; +} + +* { + box-sizing: border-box; + word-wrap: break-word; + -webkit-overflow-scrolling: touch; +} + +p, pre { + margin: 0; +} + +a { + text-decoration: none; + cursor: pointer; +} + +pre { + white-space: pre-line; +} + +code { + font-family: Consolas, Courier, monospace; +} \ No newline at end of file diff --git a/src/infrastructure/widgets.css b/src/infrastructure/widgets.css new file mode 100644 index 0000000..b439cca --- /dev/null +++ b/src/infrastructure/widgets.css @@ -0,0 +1,301 @@ +.centered-line { + overflow: hidden; + text-align: center; +} + +.centered-line::before, +.centered-line::after { + background-color: #000; + content: ""; + display: inline-block; + height: 1px; + position: relative; + vertical-align: middle; + width: 50%; +} + +.root-dark-mode .centered-line { + color: var(--foreground-dark); +} +.root-dark-mode .centered-line::before, .root-dark-mode .centered-line::after { + background-color: var(--foreground-dark); +} + +.centered-line::before { + right: 1em; + margin-left: -50%; +} + +.centered-line::after { + left: 1em; + margin-right: -50%; +} + +.title-line { + color: #fff; + margin-top: 1em; +} +.title-line::before, +.title-line::after { + background-color: #fff; + box-shadow: 0 1px 1px #000; +} + +.root-dark-mode .title-line { + color: var(--foreground-dark); +} +.root-dark-mode .title-line::before, .root-dark-mode .title-line::after { + background-color: var(--foreground-dark); +} + +.app-switcher { + display: flex; + height: 2em; + text-align: center; + margin: 0 .1em; + user-select: none; +} +.app-switcher-desc { + margin: 0 .5em; + flex: 1 1 0; + opacity: .5; + height: 2em; + line-height: 2rem; + font-size: .8em; +} + +.root-dark-mode .app-switcher-desc { + color: var(--foreground-dark); +} + +@media screen and (max-width: 570px) { + .app-switcher-desc { + flex: 1 1 0; + display: none; + } + .app-switcher-item { + flex: 1 1 0 !important; + padding: 0 !important; + } + .app-switcher-dropdown-title { + padding-left: 0 !important; + padding-right: 0 !important; + text-align: center !important; + } + .app-switcher-dropdown-item { + margin-left: -2em !important; + margin-right: 0 !important; + } +} + +.app-switcher a:hover { /* reset underline from /hole style */ + border-bottom: unset; + margin-bottom: unset; +} + +.app-switcher-desc a { + color: unset; +} + +.app-switcher-left { + text-align: right; +} +.app-switcher-right { + text-align: left; +} +.app-switcher-item { + flex: 0 0 auto; + border-radius: 3px; + height: 1.6em; + line-height: 1.6em; + margin: .2em .1em; + padding: 0 .45em; +} +a.app-switcher-item, .app-switcher-item a { + transition: unset; /* override ant design */ + color: black; +} +.app-switcher-item img { + width: 1.2rem; + height: 1.2rem; + position: relative; + top: .2rem; + vertical-align: unset; /* override ant design */ +} +.app-switcher-item span:not(:empty) { + margin-left: .2rem; +} +.app-switcher-logo-hover { + margin-left: -1.2rem; +} + +.app-switcher-item:hover { + background-color: black; + color: white !important; +} +.app-switcher-item:hover a { + color: white !important; +} +.app-switcher-item-current { + background-color: rgba(0,0,0,.4); + text-shadow: 0 0 5px rgba(0,0,0,.5); + color: white !important; +} +.app-switcher-item-current a { + color: white !important; +} + +.root-dark-mode .app-switcher-item, .root-dark-mode .app-switcher-dropdown-title a { + color: var(--foreground-dark); +} +.root-dark-mode .app-switcher-item:hover, .root-dark-mode .app-switcher-item-current, .root-dark-mode .app-switcher-dropdown-title:hover a { + background-color: #555; + color: var(--foreground-dark); +} + +.app-switcher-item:hover .app-switcher-logo-normal, .app-switcher-item-current .app-switcher-logo-normal { + opacity: 0; +} +.app-switcher-item:not(.app-switcher-item-current):not(:hover) .app-switcher-logo-hover { + opacity: 0; +} + +.root-dark-mode .app-switcher-logo-normal { + opacity: 0 !important; +} +.root-dark-mode .app-switcher-logo-hover { + opacity: 1 !important; +} + +.app-switcher-dropdown { + padding: 0; + text-align: left; +} + +.app-switcher-dropdown:not(:hover) { + max-height: 1.6rem; + overflow: hidden; +} + +.app-switcher-dropdown-item { + background-color: hsla(0,0%,35%,.9); + padding: .125em .25em; + margin-left: -.75em; + margin-right: -.75em; + position: relative; + z-index: 10; + cursor: pointer; +} +.app-switcher-dropdown-item:hover { + background-color: rgba(0,0,0,.9); +} +.app-switcher-dropdown-item:nth-child(2) { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.app-switcher-dropdown-item:last-child { + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} + +.app-switcher-dropdown-title { + padding-bottom: .2em; + padding-left: .5em; + padding-right: .25em; +} +.app-switcher-dropdown-title a { + cursor: unset; +} + +.thuhole-login-popup { + font-size: 1rem; + background-color: #f7f7f7; + color: black; + position: fixed; + left: 50%; + top: 50%; + width: 320px; + z-index: 114515; + transform: translateX(-50%) translateY(-50%); + border-radius: 5px; +} +.thuhole-login-popup a { + color: #00c; +} +.thuhole-login-popup p { + margin: .75em 0; + text-align: center; +} +/* override ant design */ +.thuhole-login-popup input, .thuhole-login-popup button { + font-size: .85em; + vertical-align: middle; +} +.thuhole-login-popup input:not([type="checkbox"]) { + width: 8rem; + border-radius: 5px; + border: 1px solid black; + outline: none; + margin: 0; + padding: 0 .5em; + line-height: 2em; +} +.thuhole-login-popup button { + width: 6rem; + color: black; + background-color: rgba(235,235,235,.5); + border-radius: 5px; + text-align: center; + border: 1px solid black; + line-height: 2em; + margin: 0 .5rem; +} +.thuhole-login-popup button:hover { + background-color: rgba(255,255,255,.7); +} +.thuhole-login-popup button:disabled { + background-color: rgba(128,128,128,.5); +} +.thuhole-login-type { + display: inline-block; + width: 6rem; + margin: 0 .5rem; +} +.thuhole-login-popup-shadow { + opacity: .5; + background-color: black; + position: fixed; + left: 0; + top: 0; + height: 100%; + width: 100%; + z-index: 114514; +} + +.thuhole-login-popup label.perm-item { + font-size: .8em; + vertical-align: .1rem; + margin-left: .5rem; +} + +.aux-margin { + width: calc(100% - 2 * 50px); + margin: 0 50px; +} +@media screen and (max-width: 1300px) { + .aux-margin { + width: calc(100% - 2 * 10px); + margin: 0 10px; + } +} + +.title { + font-size: 1.5em; + height: 4rem; + padding-top: 1rem; + text-align: center; +} + +.time-str { + color: #999999; +} \ No newline at end of file diff --git a/src/infrastructure/widgets.js b/src/infrastructure/widgets.js new file mode 100644 index 0000000..2ca90fc --- /dev/null +++ b/src/infrastructure/widgets.js @@ -0,0 +1,472 @@ +import React, {Component, PureComponent} from 'react'; +import ReactDOM from 'react-dom'; + +import TimeAgo from 'react-timeago'; +import chineseStrings from 'react-timeago/lib/language-strings/zh-CN'; +import buildFormatter from 'react-timeago/lib/formatters/buildFormatter'; + +import './global.css'; +import './widgets.css'; + +import appicon_hole from './appicon/hole.png'; +import appicon_imasugu from './appicon/imasugu.png'; +import appicon_imasugu_rev from './appicon/imasugu_rev.png'; +import appicon_syllabus from './appicon/syllabus.png'; +import appicon_score from './appicon/score.png'; +import appicon_course_survey from './appicon/course_survey.png'; +import appicon_dropdown from './appicon/dropdown.png'; +import appicon_dropdown_rev from './appicon/dropdown_rev.png'; +import appicon_homepage from './appicon/homepage.png'; +import {THUHOLE_API_ROOT} from './const'; +import {get_json, API_VERSION_PARAM} from './functions'; + +import { + GoogleReCaptchaProvider, + GoogleReCaptcha +} from 'react-google-recaptcha-v3'; + +const LOGIN_POPUP_ANCHOR_ID='pkuhelper_login_popup_anchor'; + +function pad2(x) { + return x<10 ? '0'+x : ''+x; +} +export function format_time(time) { + return `${time.getMonth()+1}-${pad2(time.getDate())} ${time.getHours()}:${pad2(time.getMinutes())}:${pad2(time.getSeconds())}`; +} +const chinese_format=buildFormatter(chineseStrings); +export function Time(props) { + const time=new Date(props.stamp*1000); + return ( + + +   + {!props.short ? format_time(time) : null} + + ); +} + +export function TitleLine(props) { + return ( +

+ {props.text} +

+ ) +} + +export function GlobalTitle(props) { + return ( +
+
+

{props.text}

+
+
+ ); +} + +const FALLBACK_APPS={ + // id, text, url, icon_normal, icon_hover, new_tab + bar: [ + ['hole', '树洞', '/hole', appicon_hole, null, false], + ['imasugu', '教室', '/spare_classroom', appicon_imasugu, appicon_imasugu_rev, false], + ['syllabus', '课表', '/syllabus', appicon_syllabus, null, false], + ['score', '成绩', '/my_score', appicon_score, null, false], + ], + dropdown: [ + ['course_survey', '课程测评', 'https://courses.pinzhixiaoyuan.com/', appicon_course_survey, null, true], + ['homepage', '客户端', '/', appicon_homepage, null, true], + ], + fix: {}, +}; +// const SWITCHER_DATA_VER='switcher_2'; +// const SWITCHER_DATA_URL=THUHOLE_API_ROOT+'web_static/appswitcher_items.json'; + +// export class AppSwitcher extends Component { +// constructor(props) { +// super(props); +// this.state={ +// apps: this.get_apps_from_localstorage(), +// } +// } +// +// get_apps_from_localstorage() { +// let ret=FALLBACK_APPS; +// if(localStorage['APPSWITCHER_ITEMS']) +// try { +// let content=JSON.parse(localStorage['APPSWITCHER_ITEMS'])[SWITCHER_DATA_VER]; +// if(!content || !content.bar) +// throw new Error('content is empty'); +// +// ret=content; +// } catch(e) { +// console.error('load appswitcher items from localstorage failed'); +// console.trace(e); +// } +// +// return ret; +// } +// +// check_fix() { +// if(this.state.apps && this.state.apps.fix && this.state.apps.fix[this.props.appid]) +// setTimeout(()=>{ +// window.HOTFIX_CONTEXT={ +// build_info: process.env.REACT_APP_BUILD_INFO || '---', +// build_env: process.env.NODE_ENV, +// }; +// eval(this.state.apps.fix[this.props.appid]); +// },1); // make it async so failures won't be critical +// } +// +// componentDidMount() { +// this.check_fix(); +// setTimeout(()=>{ +// fetch(SWITCHER_DATA_URL) +// .then((res)=>{ +// if(!res.ok) throw Error(`网络错误 ${res.status} ${res.statusText}`); +// return res.text(); +// }) +// .then((txt)=>{ +// if(txt!==localStorage['APPSWITCHER_ITEMS']) { +// console.log('loaded new appswitcher items',txt); +// localStorage['APPSWITCHER_ITEMS']=txt; +// +// this.setState({ +// apps: this.get_apps_from_localstorage(), +// }); +// } else { +// console.log('appswitcher items unchanged'); +// } +// }) +// .catch((e)=>{ +// console.error('loading appswitcher items failed'); +// console.trace(e); +// }); +// },500); +// } +// +// componentDidUpdate(prevProps, prevState) { +// if(this.state.apps!==prevState.apps) +// this.check_fix(); +// } +// +// render() { +// let cur_id=this.props.appid; +// +// function app_elem([id,title,url,icon_normal,icon_hover,new_tab],no_class=false,ref=null) { +// return ( +// +// {!!icon_normal && [ +// , +// +// ]} +// {title} +// +// ); +// } +// +// let dropdown_cur_app=null; +// this.state.apps.dropdown.forEach((app)=>{ +// if(app[0]===cur_id) +// dropdown_cur_app=app; +// }); +// +// //console.log(JSON.stringify(this.state.apps)); +// +// return ( +//
+// PKUHelper +// {this.state.apps.bar.map((app)=> +// app_elem(app) +// )} +// {!!this.state.apps.dropdown.length && +//
+//

+// {!!dropdown_cur_app ? +// app_elem((()=>{ +// let [id,title,_url,icon_normal,icon_hover,_new_tab]=dropdown_cur_app; +// return [id,title+'▾',null,icon_normal,icon_hover,false]; +// })(),true) : +// app_elem(['-placeholder-elem','更多▾',null,appicon_dropdown,appicon_dropdown_rev,false],true) +// } +//

+// {this.state.apps.dropdown.map((app)=>{ +// let ref=React.createRef(); +// return ( +//

{ +// if(!e.target.closest('a') && ref.current) +// ref.current.click(); +// }}> +// {app_elem(app,true,ref)} +//

+// ); +// })} +//
+// } +// 网页版 +//
+// ); +// } +// } + +class LoginPopupSelf extends Component { + constructor(props) { + super(props); + this.state={ + loading_status: 'idle', + recaptcha_verified: false + // excluded_scopes: [], + }; + this.username_ref=React.createRef(); + this.password_ref=React.createRef(); + this.input_token_ref=React.createRef(); + + this.popup_anchor=document.getElementById(LOGIN_POPUP_ANCHOR_ID); + if(!this.popup_anchor) { + this.popup_anchor=document.createElement('div'); + this.popup_anchor.id=LOGIN_POPUP_ANCHOR_ID; + document.body.appendChild(this.popup_anchor); + } + } + + do_sendcode(type) { + if(!this.state.recaptcha_verified) { + alert("reCAPTCHA风控系统正在评估您的浏览器安全状态,请稍后重试。") + return + } + if(this.state.loading_status==='loading') + return; + + this.setState({ + loading_status: 'loading', + },()=>{ + fetch( + THUHOLE_API_ROOT+'api_xmcp/login/send_code' + +'?user='+encodeURIComponent(this.username_ref.current.value) + +'&code_type='+encodeURIComponent(type) + +"&recaptcha_token="+localStorage["recaptcha"] + +API_VERSION_PARAM(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + excluded_scopes: [], + }), + } + ) + .then(get_json) + .then((json)=>{ + console.log(json); + if(!json.success) + throw new Error(JSON.stringify(json)); + + alert(json.msg); + this.setState({ + loading_status: 'done', + }); + }) + .catch((e)=>{ + console.error(e); + alert('发送失败\n'+e); + this.setState({ + loading_status: 'done', + }); + }); + + }); + } + + do_login(set_token) { + if(this.state.loading_status==='loading') + return; + + this.setState({ + loading_status: 'loading', + },()=>{ + fetch( + THUHOLE_API_ROOT+'api_xmcp/login/login' + +'?user='+encodeURIComponent(this.username_ref.current.value) + +'&valid_code='+encodeURIComponent(this.password_ref.current.value) + +API_VERSION_PARAM(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + excluded_scopes: [], + }), + } + ) + .then(get_json) + .then((json)=>{ + if(json.code!==0) { + if(json.msg) throw new Error(json.msg); + throw new Error(JSON.stringify(json)); + } + + set_token(json.user_token); + alert(`登录成功`); + this.setState({ + loading_status: 'done', + }); + this.props.on_close(); + }) + .catch((e)=>{ + console.error(e); + alert('登录失败\n'+e); + this.setState({ + loading_status: 'done', + }); + }); + }); + } + + do_input_token(set_token) { + if(this.state.loading_status==='loading') + return; + + let token=this.input_token_ref.current.value; + this.setState({ + loading_status: 'loading', + },()=>{ + fetch(THUHOLE_API_ROOT+'api_xmcp/hole/system_msg?user_token='+encodeURIComponent(token)+API_VERSION_PARAM()) + .then((res)=>res.json()) + .then((json)=>{ + if(json.error) + throw new Error(json.error); + if(json.result.length===0) + throw new Error('result check failed'); + this.setState({ + loading_status: 'done', + }); + set_token(token); + this.props.on_close(); + }) + .catch((e)=>{ + alert('Token检验失败\n'+e); + this.setState({ + loading_status: 'done', + }); + console.error(e); + }); + }); + } + + // perm_alert() { + // alert('如果你不需要 PKU Helper 的某项功能,可以取消相应权限。\n其中【状态信息】包括你的网费、校园卡余额等。\n该设置应用到你的【所有】设备,取消后如需再次启用相应功能需要重新登录。'); + // } + + render() { + // let PERM_SCOPES=[ + // ['score','成绩查询'], + // ['syllabus','课表查询'], + // ['my_info','状态信息'], + // ]; + + return ReactDOM.createPortal( + + { + this.setState({ + recaptcha_verified: true, + }); + localStorage["recaptcha"] = token + }} /> +
+
+
+

+ 接收验证码来登录 T大树洞 +

+

+ + + {/*this.do_sendcode('sms')}>*/} + {/*  短信 */} + {/**/} + {/*/*/} + this.do_sendcode('mail')}> +  发送邮件  + + +

+

+ + +

+
+

+ 从其他设备导入登录状态 +

+

+ + +

+
+

+ This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply. +

+

+ +

+
+
+ , + this.popup_anchor, + ); + } +} + +export class LoginPopup extends Component { + constructor(props) { + super(props); + this.state={ + popup_show: false, + }; + this.on_popup_bound=this.on_popup.bind(this); + this.on_close_bound=this.on_close.bind(this); + } + + on_popup() { + this.setState({ + popup_show: true, + }); + } + on_close() { + this.setState({ + popup_show: false, + }); + } + + render() { + return ( + <> + {this.props.children(this.on_popup_bound)} + {this.state.popup_show && + + } + + ); + } +} \ No newline at end of file