Skip to content

Commit

Permalink
v0.3.0: Папки и Треды
Browse files Browse the repository at this point in the history
  • Loading branch information
RubaXa committed Jun 16, 2016
1 parent 12f6fcf commit 99673f0
Show file tree
Hide file tree
Showing 21 changed files with 496 additions and 5 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-octavius",
"version": "0.2.0",
"version": "0.3.0",
"private": true,
"description": "Боль и унижение!",
"main": "index.js",
Expand Down
12 changes: 12 additions & 0 deletions src/actions/status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {THREADS_STATUS_MEETHOD} from '../constants/api';
import {STATUS_FETCH, STATUS_FETCH_SUCCESS, STATUS_FETCH_FAIL} from '../constants/status';

export const fetchStatus = (folder) => ({
api: THREADS_STATUS_MEETHOD,
data: {
folder: folder|0,
limit: 100,
last_modified: 1
},
types: [STATUS_FETCH, STATUS_FETCH_SUCCESS, STATUS_FETCH_FAIL],
});
51 changes: 48 additions & 3 deletions src/components/App.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,59 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import {routerActions} from 'react-router-redux';
import {fetchStatus} from '../actions/status';

import fetchData from '../decorators/fetchData';

import AuthForm from './AuthForm';
import Layout from './Layout';
import Headline from './Headline';
import Scrollable from './Scrollable';
import PortalMenu from './PortalMenu';
import Folders from './Folders';
import Letters from './Letters';

@connect(state => state)
@connect(
({auth, folders, threads}, {params}) => ({
auth,
folders,
threads: threads.current[params.folder|0] || []
}),
(dispatch) => ({
actions: bindActionCreators({fetchStatus}, dispatch),
routerActions: bindActionCreators({...routerActions}, dispatch),
})
)
@fetchData(
({auth: email}, {folder}) => ({email, folder}),
({email, folder}, actions) => email && actions.fetchStatus(folder)
)
export default class App extends Component {
handleGlobalClick(evt) {
// todo
}

render() {
const {auth} = this.props;
return !auth.state ? <AuthForm/> : <h1>Привет, {auth.email}!</h1>;
const {auth, folders, threads, params} = this.props;
const folderId = params.folder|0;

if (auth.state) {
const sidebar = <Folders models={folders} active={folderId}/>;
const main = <Letters models={threads}/>;

return <div onClick={(evt) => this.handleGlobalClick(evt)}>
<Headline/>
<PortalMenu />
<Layout
bordered
left={<Scrollable content={sidebar}/>}
main={<Scrollable content={main}/>}
/>
</div>;
} else {
return <AuthForm/>;
}
}
}
16 changes: 16 additions & 0 deletions src/components/Avatar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import classNames from 'classnames';
import React, {Component, PropTypes} from 'react';

export default class Avatar extends Component {
render() {
const {src, size} = this.props;
const classes = classNames({
'avatar': true,
'avatar_rounded': true,
[`avatar_size_${size}`]: size
});

//return <div className={classes}/>;
return <img src={(src + '').replace(/&amp;/g, '&')} draggable="false" className={classes}/>;
}
};
19 changes: 19 additions & 0 deletions src/components/Folders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, {Component, PropTypes} from 'react';

import FoldersItem from './FoldersItem';

export default class Folders extends Component {
render() {
const {models, active} = this.props;

return (
<div className="nav nav_expanded">
{models.map(folder => <FoldersItem
key={folder.id}
model={folder}
active={folder.id == active}/>
)}
</div>
);
}
}
31 changes: 31 additions & 0 deletions src/components/FoldersItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, {Component} from 'react';
import classNames from 'classnames';

import Icon from './Icon';

export default class FoldersItem extends Component {
render() {
const {model, active} = this.props;
const classes = classNames({
'nav__item': true,
'nav__item_active': active,
'nav__item_shortcut': model.system,
'nav__item_highlight': false
});

return (
<a key={model.id} href={`/${model.id}/`} className={classes}>
<span className="nav__ico">
<Icon map="folder" mod={model.type} size="m"/>
</span>

{model.messages_unread
? <span className="nav__badge">{model.messages_unread}</span>
: null
}

<span className="nav__txt">{model.name}</span>
</a>
);
}
}
3 changes: 3 additions & 0 deletions src/components/Headline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from 'react';

export default () => (<div className="headline"></div>);
25 changes: 25 additions & 0 deletions src/components/Layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import classNames from 'classnames';
import React, {Component} from 'react';

export default class Layout extends Component {
render() {
const {left, main, right, bordered} = this.props;
const classes = classNames({
'layout': true,
'layout_size_m': true,
'layout_type_2pane': true,
'layout_left-size_11': true,
'layout_right-size_12': true,
'layout_sidebar-size_12': true,
'layout_bordered': bordered
});

return (
<div className={classes}>
<div className="layout__column layout__column_left">{left}</div>
<div className="layout__main-frame">{main}</div>
<div className="layout__column layout__column_right">{right}</div>
</div>
);
}
}
33 changes: 33 additions & 0 deletions src/components/LetterStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import classNames from 'classnames';
import React, {Component} from 'react';

import Icon from './Icon';

const statusToIcoMap = {
'unread': 'letterstatus',
'flagged': 'toolbar'
};

const statusToIcoMod = {
'unread': 'unread',
'flagged': 'mark'
};

export default class LetterStatus extends Component {
render() {
const {name, state, size} = this.props;
const classes = classNames({
'letter-status': true,
[`letter-status_${name}`]: true,
[`letter-status_${name}_${state}`]: true,
});

return <div className={classes}>
<Icon
map={statusToIcoMap[name]}
mod={statusToIcoMod[name]}
size={size}
/>
</div>;
}
};
38 changes: 38 additions & 0 deletions src/components/Letters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import classNames from 'classnames';
import React, {Component} from 'react';

import LettersItem from './LettersItem';

export default class Letters extends Component {
handleToggleSelect(evt, model) {
// todo
evt.preventDefault();
}

render() {
const {models, selection} = this.props;

const classes = classNames({
'dataset': true,
'dataset_fluid': true,
'dataset_select-mode_off': true
});

const fragment = (
<div className="dataset-letters">
<div className={classes}>
<div className="dataset__items">{models.map((model) => {
return <LettersItem
key={model.id}
model={model}
selected={false}
onToggleSelect={(evt) => this.handleToggleSelect(evt, model)}
/>
})}</div>
</div>
</div>
);

return fragment;
}
}
109 changes: 109 additions & 0 deletions src/components/LettersItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import classNames from 'classnames';
import React, {Component} from 'react';

import Avatar from './Avatar';
import Button from './Button';
import Swipeable from './Swipeable';
import LetterStatus from './LetterStatus';

const DAY = 60 * 60 * 24 * 1000;
const Months = 'Янв Фев Мар Апр Мая Июн Июл Авг Сен Окт Ноя Дек'.split(' ');

function pad(number) {
return (number < 10) ? '0' + number : number;
}

function letterTime(time) {
time *= 1000;

const now = Date.now();
const date = new Date(time);

if (now - time > DAY) {
return date.getDate() + ' ' + Months[date.getMonth()];
} else {
return date.getHours() + ':' + pad(date.getMinutes());
}
}

export default class LettersItem extends Component {
render() {
const {model, selected, onToggleSelect} = this.props;
const classes = classNames({
'dataset__item': true,
'dataset__item_unread': model.flags.unread,
'dataset__item_active': false,
'dataset__item_selected': selected,
});

const actions = (
<div className="dataset__swipe-actions">
<Button ico="toolbar_unread" short borderless />
<Button ico="toolbar_mark" short borderless />
<Button ico="toolbar_remove" short borderless/>
<Button ico="toolbar_spam" short borderless/>
<Button ico="toolbar_more" short borderless/>
</div>
);

const itemRow = (
<div className="dataset__item-row">
<div className="dataset__info">
<div className="dataset__addrs">
{
model.correspondents.from[0].name ||
model.correspondents.from[0].email ||
'Неизвестно'
}
</div>

<div className="dataset__subj">
<div className="dataset__status">
<LetterStatus
name="flagged"
state={model.flags.flagged}
/>
</div>

{model.length > 1 ? <div className="dataset__badge">{model.length}</div> : null}
{model.subject}

<div className="dataset__snippet">{model.snippet}</div>
</div>
</div>

<div className="dataset__attach" dangerouslySetInnerHTML={model.flags.attach ? {__html: '&#128206'} : null}/>
<div className="dataset__date">{letterTime(model.date)}</div>
</div>
);

return (
<a
className={classes}
title={model.subject}
href={`/${model.folder}/${model.id}/`}
>
<div className="dataset__avatar-cover">
<div className="dataset__status">
<LetterStatus
name="unread"
state={model.flags.unread}
/>
</div>

<div className="dataset__avatar" onClick={onToggleSelect}>
<Avatar
size="s"
src={model.correspondents.from[0].avatars.default}
/>
</div>
</div>

<Swipeable
underlay={actions}
content={itemRow}
/>
</a>
);
}
}
31 changes: 31 additions & 0 deletions src/components/PortalMenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, {Component} from 'react';

import Button from './Button';
import Layout from './Layout';

export default class ProtalMenu extends Component {
render() {
const {selectedCount, selectionActions, threads} = this.props;
const logo = <span className="portal-menu__logo" alt="Почта@Mail.Ru" title="Почта@Mail.Ru"/>;
const actions = (<div>
<Button
onTap={(evt) => selectionActions.selectAll(threads)}
text="Выделить все"
ico="toolbar_select-all"
borderless
short
size="xl"/>

{selectedCount ? <span class="portal-menu__title">{selectedCount}</span> : null}
</div>);

return (
<div className="portal-menu">
<Layout
left={logo}
main={actions}
/>
</div>
);
}
};

0 comments on commit 99673f0

Please sign in to comment.