evectr.api
Version:
The official eVectr� API.
1,616 lines (1,519 loc) • 205 kB
JSX
'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||