UNPKG

evectr.api

Version:

The official eVectr� API.

1,616 lines (1,519 loc) 205 kB
'use strict'; module.exports = /** * Custom Components for the **eVectr** front-end Application. * @param {FluxComponents} COMPS The global Component interface. * @void */ function Comps(COMPS) { //////////////////////////////////////////////////////////////////////// // CONSTANTS ----------------------------------------------------------- const MX = COMPS.Mixins; const Mix = COMPS.Mix; const Agnostic = COMPS.Agnostic; const Agnolist = COMPS.Agnolist; const Tag = COMPS.Tag; const Actions = COMPS.Actions; const Reflux = COMPS.Reflux; const React = COMPS.React; const Frag = React.Fragment; const KIDS = React.Children; const RClone = React.cloneElement; const RDOM = COMPS.Elements.RDOM; const FA = COMPS.FA; const iURL = COMPS.iURL; const joinV = COMPS.joinV; const onBrowser = COMPS.onBrowser; const stopEvent = COMPS.stopEvent; const Uploader = COMPS.Uploader; const DATA_TMR = {}; const { StripeProvider, injectStripe: INJECT, PaymentRequestButtonElement, CardElement, CardNumberElement, CardExpiryElement, CardCVCElement, Elements } = COMPS.Elements.StripeJS; const Stripe = { Key: null }; COMPS.Elements.Evectr = {}; const EV = COMPS.Elements.Evectr; // Configs const RSignUp = '/signup'; const RLogin = '/auth/login'; const RLogout = '/auth/logout'; const RNotify = COMPS.RNotify; function getBasic (user, pass) { COMPS.Basic = 'Basic '+btoa(user.value+':'+pass.value); return COMPS.Basic; } function stripeOpts (config = {}) { return Map({ style: { invalid: { color: '#c23d4b', }, base: { color: '#424770', fontSize: '14px', letterSpacing: '0.025em', '::placeholder': { fontSize: '12px', color: '#aab7c4', }, ':-webkit-autofill': { backgroundColor:'white' } }, } }).mergeDeep(config).toJS(); }; //////////////////////////////////////////////////////////////////////// // MIXINS -------------------------------------------------------------- MX.Forms = { getAutoComp(name, props) { let hasAC = props.hasOwnProperty('complete'), compl = hasAC?props.complete:null; switch (IS(compl)) { case 'boolean': return name||'off'; case 'string': return compl||'off'; default: return null; } }, getDefault(value) { return !!value?{defaultValue:value||undefined}:{}; }, getValue(value) { return !!value?{value:value||undefined}:{}; }, getFormats() { let THS = this, props = THS.props, formatters = props.formatters, keys = Object.keys(props); if (!!formatters&&!keys.has('onChange')) { return { onChange(e) { e.stopPropagation(); e.preventDefault(); let elem = e.target; elem.value = formatters.reduce((a,c)=>( a.replace(c.pattern, c.replace) ), elem.value); } }; } else return {}; }, }; MX.BTN = { Kinds: { button: 'button', submit: 'submit', a: 'a' }, Elems: { button: 'button', submit: 'button', a: 'a' }, Types: { button: 'button', submit: 'submit', a: 'text/html' }, }; MX.Trans = { /** * @type {{[status: string]: MultiObj}} */ Status: { INQUIRED: { kind: 'info', label: 'Inquired' }, IN_REVIEW: { kind: 'info', label: 'In Review' }, BOUNCED: { kind: 'warn', label: 'Bounced' }, REJECTED: { kind: 'nope', label: 'Rejected' }, OFFER: { kind: 'info', label: 'Offer' }, ACCEPTED: { kind: 'warn', label: 'Accepted' }, DECLINED: { kind: 'nope', label: 'Declined' }, ESCROW: { kind: 'norm', label: 'Escrow' }, CANCELLED: { kind: 'nope', label: 'Cancelled' }, CONFIRMED: { kind: 'warn', label: 'Confirmed' }, DISPUTED: { kind: 'nope', label: 'Disputed' }, REFUNDED: { kind: 'mayb', label: 'Refunded' }, COMPLETE: { kind: 'good', label: 'Complete' }, }, Shared: [ 'OFFER', 'ACCEPTED', 'CANCELLED', 'CONFIRMED', ], /** * Gets the Multi-props to create a Status token with. * @param {Props.Transact.Obj} props * @returns {MultiObj} */ GetStatus(props) { let stat = props.status, { Status, Shared } = MX.Trans, rslt = Assign({}, Status[stat]); if (Shared.has(stat)) { let isMe = COMPS.UID==props.history.who; Assign(rslt, { level: ({ true: 'You', false: 'Them' })[isMe] }); }; return rslt; }, }; //////////////////////////////////////////////////////////////////////// // COMPONENTS ---------------------------------------------------------- // IMPORTS ///////////////////////////////////////////////////////// const { Defer, Tags, Bubble, NormalLink, SocketLink, PhoneNum, PhoneExt, Address, } = COMPS.Elements.Stock; // ERRORS ///////////////////////////////////////////////////////// class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } componentDidCatch(error, errorInfo) { // You can also log the error to an error reporting service console.error('REACT ERROR:', error, errorInfo); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } } // APP ///////////////////////////////////////////////////////// EV.App = class App extends Mix('Reflux','General') { /** * * @param {*} props */ constructor(props) { super(props); this.name = 'APP'; this.state = Assign({},props,{stripe:null}); this.mode = NMESPC.page.type||''; this.store = COMPS.Stores.App(props.LID); } /** * Gets the Header. * @param {("stock"|"jumbo"|"main")} mode The structural style of the page. * @param {{cover:{},user:{}}} title ghdfghfhfdgh * @param {string[]} searches gdfghdfghfghd */ getHeader(mode, title, searches = []) { let elems = []; switch (mode) { case 'cover': case 'stock': elems = elems.concat([ <Search key="sch" tokens={searches||[]}/> ]); }; switch (mode) { case 'cover': title = title||{}; elems = elems.concat([ <Frag key="plq"> <Cover uid={title.user.uid} img={title.cover} /> <Plaque {...title.user} /> </Frag>]); break;; case 'stock': elems = elems.concat( <Frag key="ttl"> <Title {...{ kind: 'page', size: 'large', mode: 'shadowed', title: title, }}/> </Frag> ); }; switch (mode) { case 'stock': case 'jumbo': elems = elems.concat([<hr key="div" className="gridItemDivide"/>]); }; return (<Frag key="hdr">{elems}</Frag>); } /** * Render the Chat Threads. * @param {("stock"|"jumbo"|"main")} mode The structural style of the page. * @param {ChatProps[]} chats A list of the user's current Message Threads. */ getThreads(mode, chats) { if (mode=='jumbo') return null; else return (<Threads {...{max:8,chats}} />); } render() { var props = this.state, mode = this.mode, header = props.header||{}, search = header.searches||[], title = header.title||null, user = header.user||{}, scopes = user.Scopes||{}, content = props.content||{}, footer = props.footer||{}, credits = footer.credits||{}, chats = footer.chats||[], ident = !!header.identified, ready = props.ready||(()=>false), classes = classN({ 'gridMain': true, 'loggedIn': ident, 'loggedOut': !ident, // 'pause': !!props.paused, 'ready': ready(), }, mode); // ------------------------------------------------- // COMPS.Token = user.Token; COMPS.IsAuthd = ident; COMPS.UID = scopes.user_id; COMPS.Email = user.Account; // ------------------------------------------------- // return (<ErrorBoundary> <main id="content" className={classes}> {this.getHeader(mode,title,search)} <Foot credits={credits} /> <Content {...content} /> <Head home={credits.website} alerts={header.alerts||{}} messages={header.messages||{}} admin={header.admin||{}} title={title} /> {this.getThreads(mode,chats)} <div className="searchDim"></div> </main> </ErrorBoundary>); } }; EV.App.Signup = class Signup extends Mix('React', 'Static') { constructor(props) { super(props); let _refs = { 'form' : React.createRef(), 'username': React.createRef(), 'password': React.createRef(), 'confirm' : React.createRef(), 'agree' : React.createRef(), }; // ---------------------------------------------------------- // this.name = 'SIGNUP'; this.handleSignup = this.handleSignup.bind(this); this._refs = _refs; this.REFS = { /** * @type {HTMLFormElement} */ get form() { return _refs.form.current; }, /** * @type {HTMLInputElement} */ get username() { return _refs.username.current; }, /** * @type {HTMLInputElement} */ get password() { return _refs.password.current; }, /** * @type {HTMLInputElement} */ get confirm() { return _refs.confirm.current; }, /** * @type {HTMLInputElement} */ get agree() { return _refs.agree.current; }, }; // ---------------------------------------------------------- // } handleSignup(e) { e.stopPropagation(); e.preventDefault(); let { username, password, confirm, agree } = this.REFS Actions.Data.send(RSignUp, { method: 'POST', body: { email: username.value, password: password.value, confpass: confirm.value, agree: !!agree.checked, } }, false); } render() { let THS = this, REFS = this._refs, props = this.props, size = props.size||'some', start = props.start||'eight', name = 'user-signup', styles = ['spread','gridSlice','spaced'], align = classN(start,size), terms = props.terms||[], attrs = { 'id': name, 'name': 'signup', 'data-action': '/signup', 'method': 'POST', 'className': classN(styles), 'buttons': [{ kind: "submit", label: "Sign Up!", styles: ['good'], large: true, block: true, start, size, }], }; return ( <Form key='signupfrm' ref={REFS.form} {...attrs}> <Form.Xput {...{id: 'signup-email', name: 'email', kind: 'email', icon: 'envelope', styles: align, placeholder:'you@email-domain.com', complete: 'username email', required: true, priority: '*', }} forRef={REFS.username} /> <Form.Xput {...{id: 'signup-password', name: 'password', kind: 'password', icon: 'key', styles: align, placeholder:'Password', complete: 'new-passowrd', required: true, priority: '*', }} forRef={REFS.password} /> <Form.Xput {...{id: 'signup-confirm', name: 'confpass', kind: 'password', icon: 'unlock-alt', styles: align, placeholder:'Confirm Password', complete: 'new-passowrd', required: true, priority: '*', }} forRef={REFS.confirm} /> <Form.Checkbox {...{id: 'signup-agree', name: 'agree', label: 'I agree to these Terms:', styles: [start,size,'good-y','nope-n'], required: true, yes: 'YES', no: 'NO', }} forRef={REFS.agree} /> <div className="code one more reach" style={{fontSize:'.9rem',gridRowEnd:7}}> <div style={{paddingLeft:0}}> {terms.map((t,i) => Agnostic(t,i))} </div> </div> <hr className={align}/> </Form> ); } } EV.App.Login = class Login extends Mix('React', 'Static') { constructor(props) { super(props); let _refs = { 'form': React.createRef(), 'username': React.createRef(), 'password': React.createRef(), 'remember': React.createRef(), }; this.name = 'LOGIN'; this.id = 'user-login', this.handleLogin = this.handleLogin.bind(this); this._refs = _refs; this.REFS = { /** * @type {HTMLFormElement} */ get form() { return _refs.form.current; }, /** * @type {HTMLInputElement} */ get username() { return _refs.username.current; }, /** * @type {HTMLInputElement} */ get password() { return _refs.password.current; }, /** * @type {HTMLInputElement} */ get remember() { return _refs.remember.current; }, }; } handleLogin(e) { e.stopPropagation(); //e.preventDefault(); let enc, { username:usr, password:pss, remember:rem, form } = this.REFS; usr.disabled = pss.disabled = true; enc = `'Basic ${btoa(`${usr.value}:${pss.value}`)}`; usr.value = pss.value = ''; Actions.Data.auth(RLogin, { method: 'POST', headers: { authorization: enc }, body: { remember: rem.checked, id: this.id } }, false); form.submit(); } getAutoCompFix(name, source) { return (<iframe key={name} {...{ src: source, id: name, name: name, style: {display:'none'} }}/>); } render() { let THS = this, REFS = THS._refs, name = THS.id, props = THS.props, size = props.size||'some', start = props.start||'one', styles = ['spread','gridSlice'], align = classN(start,size), autoc = 'auto-login', source = '/public/html/auto.htm', attrs = { 'id': name, 'name': 'login', 'action': source, 'data-action': '/login', 'method': 'post', 'encType': 'multipart/form-data', 'target': autoc, 'className': classN(styles), 'onSubmit': THS.handleLogin, }; return ([ this.getAutoCompFix(autoc, source), <form key='loginfrm' ref={REFS.form} {...attrs}> <div className={classN(['submit'],start,size)} style={{order:1000}}> <Form.Button kind="submit" label="Login!" styles={['info']} large block action={THS.handleSignup}/> </div> <Form.Xput {...{id: 'login-email', kind: 'email', icon: 'envelope', styles: align, placeholder:'you@email-domain.com', complete: 'username email', required: true, priority: '*', }} forRef={REFS.username} /> <Form.Xput {...{id: 'login-password', kind: 'password', icon: 'key', styles: align, placeholder:'Password', complete: 'current-password', required: true, priority: '*', }} forRef={REFS.password} /> <Form.Checkbox {...{id: 'login-remember', label: 'Remember Me:', styles: [start,size,'good-y','info-n'], complete: true, yes: 'YES', no: 'NO', }} forRef={REFS.remember} /> <hr className={align}/> </form> ]); } } EV.App.Logout = class Logout extends Mix('React', 'Static') { constructor(props) { super(props); this.name = 'LOGOUT'; this.id = 'user-logout'; this.handleLogout = this.handleLogout.bind(this); } handleLogout(e) { e.preventDefault(); e.stopPropagation(); let req = { method: 'POST', headers: { token: COMPS.Token }, body: { id: this.id } }; Actions.Data.auth(RLogout, req, true); return false; } render() { let THS = this, props = THS.props, name = THS.id, tab = props.tabIndex, autoc = 'autocomp', source = '#', attrs = { 'id': name, 'name': name, 'action': source, 'data-action': '/logout', 'method': 'POST', 'target': autoc, 'onSubmit': THS.handleLogout, 'tabIndex': (tab||'').toString(), }; return (<form key='auth' {...attrs}>{props.children}</form>); } }; EV.App.Sessions = class Sessions extends Mix('Reflux','Static') { constructor(props) { super(props); let THS = this; THS.name = 'SESSIONS'; // --------------------------------------------------- THS.id = 'user-sessions-tbl'; // --------------------------------------------------- THS.mapStoreToState(COMPS.Stores.Data, store => { let { stamp, items = {} } = (store[THS.id]||{}); if (!!stamp&&stamp!==THS.state.stamp) return { loaded: true, status: 'done', list: items.user||[], stamp, }; else return null; } ); } // CYCLE ///////////////////////////////////////////////////////// static getDerivedStateFromProps(props, state) { if (props.stamp !== state.stamp) { let { stamp, status, list } = ( props.stamp>state.stamp?props:state ); return { stamp: stamp, status: status, list: list, }; }; return null; } componentDidMount() { let prop = this.state, send = Actions.Data.auth, load = !!prop.loaded; if (!!document&&!load) { let url = '/auth/sessions', id = this.id; setTimeout(() => send(url, { method: 'GET', headers: { token: COMPS.Token }, params: {}, query: { id }, } ), 1000); } } // FUNCTION ///////////////////////////////////////////////////////// /** * Renders the user-sessions table. * @param {Object} props The component properties. * @param {{}[]} props.list A list of user sessions. */ SessList({ id, list = [], stamp, status } = props) { let data, rslt, NA = 'N/A', cnt = list.length, empt = cnt==0, SSID = '', emph = (SS) => (SS.current?{fontStyle:'italic'}:{}), ofst = (new Date().getTimezoneOffset()*60000), dig2 = '2-digit', dopt = [ 'en-US', { year: dig2, month: dig2, day: dig2, hour: dig2, minute: dig2, } ], clnm = { ipa:"IP",dev:"Device",brw:"Browser",snc:"Since",act:"..." }, dflt = FromJS({ cols: ['.8fr','auto','1fr','1fr','.4fr'], items: [{ [clnm.ipa]: { text: "" }, [clnm.dev]: { text: "" }, [clnm.brw]: { text: "No Sessions Here!" }, [clnm.snc]: { text: "" }, [clnm.act]: { text: "" }, } ] }), itms = list.map((SS,i,_a,B) => (B=emph(SS), SS.current&&(SSID=SS.ssid), ({ [clnm.ipa]: { style: B, text: SS.ip||NA }, [clnm.dev]: { style: B, text: !!SS.browser ? (`${SS.device} [${SS.os}]`).replace('[]','').trim()||NA : NA }, [clnm.brw]: { style: B, text: SS.browser||SS.device||NA }, [clnm.snc]: { style: B, text: new Date(SS.since+ofst).toLocaleString(...dopt) }, [clnm.act]: { style: B, text: (<Frag> <input key="ssid" type="hidden" name={`ssid@${i}`} value={SS.ssid} data-param/> <Form.Button key="btn" styles={['nope']} kind="submit" id={`Kill@${i}`} action={(e)=>(e.currentTarget.dataset.id=i)} font=".5rem" icon="trash" disabled={SS.current} /> </Frag>)} }))), clra = (!empt ? [{ [clnm.ipa]: { text: "" }, [clnm.dev]: { text: "" }, [clnm.brw]: { text: "" }, [clnm.snc]: { text: "Clear All Devices", style: { fontWeight:'bold' } }, [clnm.act]: { text: (<Frag> <input key="vert" type="hidden" name={`invert@${cnt}`} value="true" /> <input key="ssid" type="hidden" name={`ssid@${cnt}`} value={SSID} data-param /> <Form.Button key="btn" styles={['nope']} kind="submit" id={`Kill@All`} action={(e)=>(e.currentTarget.dataset.id=i)} font=".5rem" icon="trash" /> </Frag>) } }] : []); // ----------------------------------------------------------- data = FromJS(Assign({ items: [...itms, ...clra] },{ form: { 'id': `${id}-form`, 'rid': `${id}`, 'method': 'DELETE', 'data-action': `/auth/sessions`, 'data-differ': true, 'api': 'auth', 'params': {}, 'query': {}, 'stamp': stamp, 'status': status, 'style': { fontSize: 'small' }, } })); // ----------------------------------------------------------- rslt = dflt.mergeDeepWith((o,n)=>n||o,data).toJS(); // ----------------------------------------------------------- return (<Content.Table2 key="sess" {...rslt} />); }; // MAIN ///////////////////////////////////////////////////////// render() { let THS = this, ID = THS.id, SESS = THS.SessList, { list, stamp, status } = THS.state; return <SESS key="sess" id={ID} list={list} stamp={stamp} status={status}/>; } }; EV.App.Sessions.defaultProps = { stamp: new Date(), status: 'load', loaded: false, }; EV.App.Modal = class Modal extends Mix('React', 'Static') { constructor(props) { super(props); this.name = 'PORTAL'; // -------------------------------------------------- this.Root = document.getElementById('modal-root'); this.Elem = document.createElement('div'); } componentDidMount () { let { Root, Elem, props } = this; Elem.setAttribute("role","dialogue"); Elem.setAttribute("aria-label",props.label); Root.appendChild(Elem); } componentWillUnmount() { this.Root.removeChild(this.Elem); } render() { let { Elem, props } = this, kids = props.children; return RDOM.createPortal(kids, Elem); } }; EV.App.Dialogue = function Dialogue({ show = [], steps = [], step = 1, closer } = props) { // ---------------------------------------------------------------------------------- // steps = steps.filter(s=>!!s); // ---------------------------------------------------------------------------------- // let cnts = steps.length, grid = { style: { gridTemplateColumns: `repeat(${cnts}, auto)` } }, stat = { true: { className: 'current trunc' }, false: { className: 'trunc' } }, stpr = (s,i)=>(s.step=(i+1), s.show=(step==s.step), s.key=`step-${s.step}`, s); // ---------------------------------------------------------------------------------- // steps = steps.map(stpr); // ---------------------------------------------------------------------------------- // return (!!show ? ( <App.Modal step={step} label={steps[step-1].title}> <div className="dialog header" data-step={step}> <h6 className="heading trunc noSelect" {...grid}> {steps.filter(s=>!!!s.hidden).map(s=>( <span key={s.key} {...stat[s.show]}><span className="trunc">{s.title}</span></span> ))} </h6> <button title="Close this Window" className="close tkn nope" onClick={closer}>X</button> </div> <div className="dialog box" data-step={step} {...grid}>{steps.map((s,i)=>( <div key={`step${i}`} className={classN({show:s.show})}>{s.content}</div> ))}</div> </App.Modal> ) : null); }; // HEAD ///////////////////////////////////////////////////////// EV.Head = function Head(props) { let mode = NMESPC.page.type||'', title = props.title, home = props.home, msgs = props.messages, alert = props.alerts, admin = props.admin, jumbo = mode=='jumbo', space = jumbo?'S':'B', flex = ['flex','flexDirRow',`flexSpace${space}`], clss = ['noSelect','gridItemBranding'], style = { // backgroundImage: `url('public/images/Logo.png')` }; return ( <header className={classN('gridItemHeader','gridHeader')} id="header"> {/* <!-- BANNER --> */} <section className={classN(...clss.concat(flex))} id="banner" role="banner"> <a href={`http://${home}`}> <div id="logo" className="gpu" style={style} role="logo"></div> </a>{jumbo && !!title ? <header><h1 className="title gpu"><span className="trunc">{title}</span></h1></header> : null} </section> {/* <!-- NAVIGATION --> */} { !jumbo ? <nav className="gridItemNav gridTabs compact" tabIndex="0" role="menubar"> <Head.SearchBtn /> <Head.Drop {...msgs} /> <Head.Drop {...alert} /> <Head.Drop {...admin} /> <input type="radio" className="ctrl" name="navDrops" id="navNone" tabIndex="1" defaultChecked/> </nav> : null} </header> ); }; EV.Head.defaultProps = { title: '', home: '', messages: [], alerts: [], admin: [], }; EV.Head.SearchBtn = function SearchBtn({} = props) { return (COMPS.IsAuthd ? <div className="gridDrop"> <label role="menuitem" id="btnSearch"> <a id="gotoSearch" className={FA('search')} href="#app-root"></a> </label> </div> : null) }; EV.Head.Drop = class Drop extends Mix('React', 'Static') { constructor(props) { super(props); this.name = 'DROP'; } render() { let props = this.props, state = this.state, load = state._meta.load, group = props.group, id = props.id, lid = `${id}-lbl`, mid = `${id}-mnu`, tab = props.tab, icon = props.icon, label = props.label, igroup= props.igroup, items = props.items||[], all = (!!props.all?{ id: `${igroup}-${items.length}`, igroup: igroup, href: props.all, label: props.allLbl, }:null), DItem = Head.Drop[props.kind||'MItem']; return ( <div className="gridDrop"> <input type="radio" className="reveal" name={group} id={id} aria-hidden="true" role="presentation"/> <label id={lid} className="reveal" htmlFor={id} tabIndex={tab} role="menuitem" aria-haspopup="true" aria-controls={mid}> <i className={FA(icon)} role="none"></i> {!!label?(<span className="hidden-xs hidden-sm" role="none">{label}</span>):null} </label> <Defer load={load} what={()=>( <div id={mid} className="drop reveal" role="presentation"> <div className="menu" aria-labelledby={lid} role="menu"> {items.map((v,i) => { let id = `${igroup}-${i}`; return (<DItem key={id} id={id} group={igroup} tab={tab} {...v}/>); }).concat(!!all?[ <Head.Drop.ALL key={all.id} {...all} tab={tab} /> ]:[])} </div> </div> )} /> </div> ); } }; EV.Head.Drop.defaultProps = { _meta: { load: false }, group: null, id: null, tab: '0', icon: null, label: '', igroup: '', items: [], all: '', }; EV.Head.Drop.MItem = function MItem({ MenuItem, BtnElem, hasOpt = true, group, id, tab, href } = props) { let BProps = BtnElem.props||{}, BTag = Tag(BtnElem.tag); return (<Frag> { !!hasOpt ? <input type="radio" className="ctrl" name={group} id={id} aria-hidden="true" role="presentation"/> : null }<BTag {...BProps} href={href} tabIndex={tab} role="menuitem">{MenuItem}</BTag> </Frag>); }; EV.Head.Drop.MSG = function MSG({ group, id, tab, href, time, label, detail } = props) { return ( <Head.Drop.MItem {...{ group, id, tab, href }} MenuItem={( <div className="message prev"> <header> <strong>{label}</strong> <span className="pull-right muted"> <em>{time}</em> </span> </header> <div>{detail}</div> </div> )} BtnElem={{ tag: 'a' }} /> ); }; EV.Head.Drop.MSG.defaultProps = { group: null, id: null, tab: '0', icon: null, label: '', href: '#', time: '', detail: '', }; EV.Head.Drop.ALRT = function ALRT({ group, id, tab, href, time, label, icon } = props) { return ( <Head.Drop.MItem {...{ group, id, tab, href }} MenuItem={( <div className="notification"> <i className={FA(icon)}></i>{` ${label.trim()}`} <span className="pull-right muted small">{time}</span> </div> )} BtnElem={{ tag: 'a' }} /> ); }; EV.Head.Drop.ALRT.defaultProps = { group: null, id: null, tab: '0', icon: null, label: '', href: '#', time: '', }; EV.Head.Drop.BTN = function BTN({ group, id, tab, href, kind, label, icon, wrap } = props) { let Kind = MX.BTN.Kinds[kind||'a'], hasWrap = !!wrap, mattrs = { group, id, tab, href }, iattrs = { tag: MX.BTN.Elems[Kind], props: { type: MX.BTN.Types[Kind] } }, attrs = (hasWrap ? wrap : iattrs), item = ( <label htmlFor={id}> <i className={FA(icon)}></i>{` ${label.trim()}`} </label> ); return ( <Head.Drop.MItem {...mattrs} MenuItem={( hasWrap ? (<Head.Drop.MItem {...mattrs} MenuItem={item} BtnElem={iattrs} hasOpt={false} />) : item )} BtnElem={attrs} /> ); }; EV.Head.Drop.BTN.defaultProps = { group: null, id: null, tab: '0', icon: null, label: '', href: '#', }; EV.Head.Drop.ALL = function ALL({ group, id, tab, href, label, icon } = props) { return ( <Head.Drop.MItem {...{ group, id, tab, href }} MenuItem={(<div className="msg all"><strong>{label}</strong> <i className={FA(icon)}></i></div>)} BtnElem={{ tag: 'a' }} /> ); }; EV.Head.Drop.ALL.defaultProps = { group: null, id: null, tab: '0', icon: 'angle-right', label: 'Read All Messages', href: '#', }; // SEARCH ///////////////////////////////////////////////////////// EV.Search = function Search(props) { let id = "search", name = "terms", attrs = { id:id,name:id,method:'POST',action:'/results',accept:'text/html'}, classes = ["gridItemSearch","gridSearch"], tokens = props.tokens||[], isSrch = NMESPC.name == 'results'; return ( COMPS.IsAuthd ? <form {...attrs} className={classN('norm-b',...classes)} role="search"> <div className="tkn norm" role="presentation"><span><i className={FA('search')}></i></span></div> <Form.Tokens {...{ kind: "token", search: true, id: name, name: name, placeholder: ["i.e. New York, French, Tutor, etc."], // placeholder: ["I'm looking for People who...","and/or..."], styles: ["gridItemSearchBox","bare"], complete: "off", tokens: tokens, clear: true, verbs: true, removal: "delete", more: ['Casual'], data: { id: `${name}-sgst`, url: '/search/suggest', context: true, }, }}/> <input type="hidden" id="search-uid" name="uid" value={COMPS.UID} required/> <button type="submit" id="search-go" className="tkn norm"> <span>GO</span>{ !isSrch ? <Frag> <a href="/defined">Defined Search</a></Frag> : null } </button> </form> : <div className={classN(...classes)} role="presentation"></div> ); } // COVER ///////////////////////////////////////////////////////// EV.Cover = class Cover extends Mix('Reflux', 'Static') { /** * @param {{uid:number,img:string,load:boolean}} props */ constructor(props) { super(props); let THS = this; // --------------------------------------------- // THS.name = 'COVER'; THS.id = 'user_cover'; // --------------------------------------------- // THS.handlePic = Uploader( `/user/${COMPS.UID}/photos/cover`, "PUT", THS.id, {token:COMPS.Token} ).bind(THS); // --------------------------------------------- // THS.mapState({ [THS.id]: { default: {}, /** * @param {Props.User.Obj} user */ state({ photos = {} } = user) { let { cover } = photos; if (!!cover) return { img: `${cover}?v=${(new Date().getTime())}` }; else return null; } }, }); } /** * Renders a User's Cover Pic. Editable; if applicable. * @param {Object} props * @param {string} props.image * @param {boolean} props.isUser * @param {(e:Event)=>void} props.onSubmit */ Picture({ image, isUser = false, onSubmit } = props) { let id = "cover_file", style = (!!image ? { backgroundImage: `url('${image}')` } : null); return ( <header className={classN("gridItemCover",{edit:isUser})} role="complementary"> <div className="gpu" style={style} role="img"></div> { isUser ? <Frag> <form className="coverForm" encType="multipart/form-data"> <input type="hidden" name="single" value="true" required/> <input type="file" id={id} name="cover" onChange={onSubmit} required/> </form> <label className="coverLbl btn tkn norm some" htmlFor={id}> <span>Edit</span> </label> </Frag> : null } </header> ); } render() { let { state, props, handlePic, Picture } = this, uid = props.uid, isUser = COMPS.UID == uid, image = state.img||''; console.log('COVER:', Assign({}, props)) return ( <Picture {...{ image, isUser }} onSubmit={handlePic}/> ) } }; EV.Cover.defaultProps = { img: '', load: false, } // TITLE ///////////////////////////////////////////////////////// EV.Title = class Title extends Mix('React', 'Static') { constructor(props) { super(props); this.name = 'TITLE'; } getSub(subtitle, isShadow) { let res = Assign( { label: '', badges: []}, FromJS(subtitle||{}).toJS() ); if (!!res.label ) res.label = ['\u000a(',res.label,')']; if (!!!res.badges) res.badges = []; else if (isShadow) res.badges = res.badges.map(v => ( v.kind = v.kind/*.split('').reverse().join('')*/,v) ); return res; } getFull(isShadow, title, subtitle) { return (isShadow?[title].concat(subtitle.label).join(''):null); } getAttrs(isShadow, id, title, subtitle) { return { className: classN('gridItemName',isShadow?'d':''), 'aria-hidden': isShadow.toString(), 'aria-labelledby': isShadow?null:id, 'data-full': this.getFull(isShadow, title, subtitle), }; } getTitle(props, isShadow = false, size = 'large') { let id = props.id, kind = props.kind||'page', title = props.title, subtitle = this.getSub(props.subtitle, isShadow), isUser = kind=='user', shadow = isShadow?'noShadow':null, attrs = this.getAttrs(isShadow, id, title, subtitle), subs = (isUser?( <sup aria-hidden="true"> <span>{subtitle.label[1]||''}</span> <Frag> {subtitle.badges.map((v,i) => <Trusts.Badge key={`badge-${i}`} {...v}/> )} </Frag> </sup> ):null); return (size!='small' ? ( <header {...attrs} role="complementary"> <div className={shadow}> <h1 id={isShadow?null:id} className="gpu"> <span className="trunc">{title}</span> {subs} </h1> </div> </header> ) : ( <Frag> <h3 className="gpu" role="complementary"><span className="trunc">{title}</span></h3> <h3>{subs}</h3> </Frag> )); } render() { let props = this.props, size = props.size, isShadow = (props.mode=='shadowed'), isOnly = (props.mode=='shadow-only'); return ( <Frag> {isShadow?this.getTitle(props, true, size):null} {this.getTitle(props, isOnly, size)} </Frag> ); } } EV.Title.defaultProps = { id: 'pageTitle', kind: 'page', mode: 'shadowed', title: '...', size: 'large', } // PLAQUE ///////////////////////////////////////////////////////// EV.Plaque = class Plaque extends Mix('Reflux', 'Static') { constructor(props) { super(props); let THS = this; // --------------------------------------------- // THS.name = 'PLAQUE'; THS.id = 'user_photo'; // --------------------------------------------- // THS.handleChat = THS.handleChat.bind(THS); THS.handleJoin = THS.handleJoin.bind(THS); THS.handlePic = Uploader( `/user/${COMPS.UID}/photos/picture`, "PUT", THS.id, {token:COMPS.Token} ).bind(THS); // --------------------------------------------- // THS.mapState({ [THS.id]: { default: {}, /** * @param {Props.User.Obj} user */ state({ photos = {} } = user) { let { profile } = photos; if (!!profile) return { photo: `${profile}?v=${(new Date().getTime())}` }; else return null; } }, }); } /** * Starts a new Chat with the User of this Profile. * @param {React.UIEvent} e The click event. * @void */ handleChat(e) { stopEvent(e); let { uid } = this.props, req = { method: 'POST', headers: { token: COMPS.Token }, params: { uids: [uid].join(';') }, body: { id: 'newchat' }, }; console.log('CHAT REQ:', req) Actions.Data.send('/threads', req, true); } /** * Add the User of this Profile to the current User's Community. * @param {React.UIEvent} e The click event. * @void */ handleJoin(e) { stopEvent(e); } /** * Renders a User's Profile Pic. Editable; if applicable. * @param {Object} props * @param {string} props.name * @param {string} props.pic * @param {boolean} props.isUser * @param {(e:Event)=>void} props.onSubmit */ Picture({ name, pic, isUser = false, onSubmit } = props) { let id = "picture_file"; return ( <header className="gridItemPic" role="complementary"> <Bubble kind="user" name={name} img={pic} opts={['huge','cutout',{edit:isUser}]} id="profile_picture" htmlFor={id}/> { isUser ? <form encType="multipart/form-data"> <input type="hidden" name="single" value="true" required/> <input type="file" id={id} name="picture" onChange={onSubmit} required/> </form> : null } </header> ); } render() { let { props, state, handleChat, handleJoin, handlePic, Picture } = this, uid = props.uid, isUser = COMPS.UID == uid, mode = props.mode||'show', pic = state.photo, uname = props.uname||'', fname = props.name, badges = props.badges||[], locale = props.locale, age = props.age, sex = { M: 'mars', F: 'venus', I: 'transgender-alt' }[props.sex]; return ( <Frag> {/* <!-- PROFILE PHOTO --> */} <header className="gridItemPic d" role="complementary"> <Bubble kind="user" name={fname} img={pic} opts={['small','er','lite']} /> </header> <Picture name={fname} {...{ pic, isUser }} onSubmit={handlePic}/> {/* <!-- PROFILE INFO --> */} <Title {...{ id: 'profileName', kind: 'user', mode: 'shadowed', title: joinV(props.name), subtitle: { label: uname, badges: badges }, }}/> <header className="gridItemInfo PLR" role="complementary"> <div className="noShadow"> <h4>{locale}</h4> <h6>{!!sex?<i className={FA(sex)}></i>:null} {!!age?<Frag> {age}<sup>years old</sup> </Frag>:null} </h6> </div> </header> {/* <!-- PROFILE SOCIAL --> */} {mode=='show' && !isUser ? ( <header className="gridItemJoin" role="complementary"> <div className="cutout"> <button className="tkn mayb large" title={`Chat with ${uname}!`} onClick={handleChat}> <span><i className={FA('comments')}></i></span> </button> </div> <div className="cutout"> <button className="tkn good large block" title={`Invite ${uname} into your world!`} onClick={handleJoin}> <span><i className={FA('user-plus')}></i> Join my Community</span> </button> </div> </header> ) : null } </Frag> ); } }; EV.Plaque.Stub = class Stub extends EV.Plaque { constructor(props) { super(props); this.name = 'STUB'; } render() { let props = this.props, pic = props.Photo, user = props.Account, name = joinV(props.Name), badges = props.Badges, locale = joinV(props.Location,', '), age = props.Age, sex = { M: 'mars', F: 'venus', I: 'transgender-alt' }[props.Sex], multis = props.Multis; return ( <a href={props.href||'#'} className="gridStub spread"> {/* <!-- STUB PHOTO --> */} <Bubble kind="user" opts={['medium','dark']} name={props.Name} img={pic}/> {/* <!-- STUB INFO --> */} <Title {...{ kind: 'user', size: 'small', mode: null, title: name, subtitle: { label: user, badges: badges }, }}/> <h6><span className="trunc">{locale}</span></h6> <h6>{!!sex?<i className={FA(sex)}></i>:null} {!!age?<Frag> {age}<sup>years old</sup> </Frag>:null} </h6> <div><div>{multis.map((v,i) => ( <Content.Multi key={`stub-${i}`} weight="small" {...v}/> ))}</div></div> </a> ); } }; EV.Plaque.defaultProps = { Account: '--{{ACCOUNT}}--', Photo: '--{{PHOTO}}--', Name: { First: '--{{NAME.FIRST}}--', Last: '--{{NAME.LAST}}--' }, Badges: [], Age: 0, Sex: 'I', Email: '--{{EMAIL}}--', Location: { City: '--{{LOCATION.CITY}}--', Region: '--{{LOCATION.REGION}}--', Country:'--{{LOCATION.COUNTRY}}--' }, }; EV.Plaque.Stub.defaultProps = { Account: '--{{ACCOUNT}}--', Photo: '--{{PHOTO}}--', Name: { First: '--{{NAME.FIRST}}--', Last: '--{{NAME.LAST}}--' }, Badges: [], Age: 0, Sex: 'I', Location: { City: '--{{LOCATION.CITY}}--', Region: '--{{LOCATION.REGION}}--', Country:'--{{LOCATION.COUNTRY}}--' }, Multis: [], }; // CONTENT ///////////////////////////////////////////////////////// EV.Content = class Content extends Mix('React', 'Static') { constructor(props) { super(props); this.name = 'CONTENT'; } getSegments(segments = {}) { return Map(segments).map((s,n)=>( IS(s)=='object'?s:{name:n,items:s} ) ).toObject(); } getSideBar(segments = {}) { let { copy, other, sidebar } = segments; if (!!sidebar&&!!sidebar.items) return sidebar; else { copy = copy||{items:[]}; other = other||{items:[]}; let dflts = { label: '', href: '#', icon: null }, items = copy.items.concat(other.items).filter(v=>!!v); return { name: 'sidebar', items: items.map(v => { let prps = (v||{}).props||{}, head = prps.header||{}, href = {href:`#${prps.name}`}; return (!!head?Assign({},dflts,head,href):null); }).filter(v=>!!v) }; } } render() { let { SideBar, Copy, Other } = Content; let props = this.props, sgmnt = this.getSegments(props.segments), { copy, other } = sgmnt, sideb = this.getSideBar(sgmnt), jumbo = NMESPC.page.type=='jumbo', style = props.style; return ( <section className="gridItemContent gridContent" style={style} role="main"> {!jumbo?<SideBar {...sideb||[]} />:null} <Copy {...copy ||[]} /> {!jumbo?<Other {...other||[]} />:null} </section> ); } }; EV.Content.SideBar = class SideBar extends Mix('React', 'Static') { constructor(props) { super(props); this.name = 'SIDEBAR'; } render() { let props = this.props, name = props.name||'', bttns = props.items||[]; return ( <nav id={name} className="gridItemSidebar gridMenu" aria-controls="copy other"> {bttns.map((v,i) => ( <Frag key={i}> <label key={`sbttn-${i}`} className={classN("btn",!!v.small?'sub':null)}> <a href={v.href}>{`${v.label} `} {!!v.icon?(<i className={FA(v.icon)}></i>):null} </a> </label> {(v.subs||[]).map((s,k) => ( <label key={`sbttn-${i}${k}`} className="btn sub"> <a href={`#${s.name}`}>{`${s.label} `} {!!s.icon?(<i className={FA(s.icon)}></i>):null} </a> </label> ))} </Frag> ))} </nav> ); } }; EV.Content.Copy = class Copy extends Mix('React', 'Static') { constructor(props) { super(props); this.name = 'COPY'; } render() { let props = this.props, name = props.name, panels = props.items||[], form = props.form||{}, Elem = !!props.form?Form:'section'; return ( <Elem id={name} className="gridItemCopy PLR" {...form}> {panels.map((v,i) => Agnostic(v, i))} </Elem> ); } }; EV.Content.Other = class Other extends Mix('React', 'Static') { constructor(props) { super(props); this.name = 'OTHER'; } render() { let props = this.props, name = props.name, panels = props.items||[]; return ( <aside id={name} className="gridItemOther" aria-hidden="true"> {panels.map((v,i) => Agnostic(v, i))} </aside> ); } }; EV.Content.Panel = class Panel extends Mix('React', 'Static') { constructor(props) { super(props); this.name = 'PANEL'; } render() { let props = this.props, name = props.name, head = props.header, body = props.body, accrd = !!props.accordian?'accordian':null, kind = classN("panel",props.kind||'',{full:!!props.full},{trail:!!props.trail}), fixed = !!(head||{}).fixed?'fixed':null, align = props.align||'', form = props.form||null, Elem = !!form?Form:'article'; return ( <section id={name} className={kind}> {!!head?<header className={classN('heading',fixed)}> <h3>{head.label}{!!head.icon?<i className={FA(head.icon)}></i>:null}</h3> </header>:null} <Elem className={classN('body',align,accrd)} {...form}> {body.map((v,i) => Agnostic(v, i))} </Elem> </section> ); } }; EV.Content.Slab = class Slab extends Mix('React', 'Static') { constructor(props) { super(props); let THS = this; THS.name = 'SLAB'; THS.kind = {true:'radio',false:'checkbox'}; THS.clss = {true:'swap',false:''}; THS.icon = {'data-open':'','data-close':''}; THS.hcls = "heading reveal tkn norm block close"; } getContent({ load = false, children }) { return ((!!load ? ( <div key="ctn" className="body gridSlice gap reveal" aria-hidden="true"> {children} </div> ) : null)); } render() { let THS = this, Elem = THS.getContent, hclass = THS.hcls, { title, id, group, children, load, swap } = THS.props; return ( <div key={id} className={classN("panel","slab","block",THS.clss[!!swap])}> <input type={THS.kind[!!swap]} id={id} name={group||id} className="reveal open"/> { !!swap ? <input type="radio" id={`${id}-cls`} name={group||id} className="reveal close"/> : null } <label className={hclass} htmlFor={id}><h6>{title}</h6></label> { !!swap ? ( <label htmlFor={`${id}-cls`} className={hclass}><h6 {...THS.icon}></h6></label> ) : null } <Elem key="content" load={load}>{children}</Elem> </div> ); } }; EV.Content.Slab.defaultProps = { load: false }; EV.Content.Trader = class Trader extends Mix('React', 'Static') { constructor(props) { super(props); this.name = 'TRADER'; } get styles () { return ['tkn','block','reveal']; } render() { let THS = this, props = THS.props, id = props.id, divide = !!props.divide, styles = this.styles.concat(props.style||