Merge branch 'master' into feat/modal/add_position

This commit is contained in:
Jalil Arfaoui 2016-12-20 16:40:30 +01:00
commit ac9e7c3e12
65 changed files with 982 additions and 934 deletions

View file

@ -31,38 +31,53 @@
"jsx": true
},
"globals": {
"document": false,
"window": false,
"global": false,
"require": false,
"expect": false,
"should": false,
"chai": false,
"sinon": false,
"Power2": false,
"Sine": false,
"__DEVELOPMENT__": false
},
"rules": {
"max-len": [0],
"new-cap": ["warn", {"capIsNewExceptions": [
"Immutable",
"Map",
"List",
"Set",
"OrderedSet",
"Range",
"Moment",
"Linker",
"Moment"
]}],
"comma-dangle": ["warn", "always-multiline"], // disallow or enforce trailing commas
"curly": ["warn", "multi-or-nest"], // specify curly brace conventions for all control statements
"indent": ["warn", 2, {"SwitchCase": 1}], // this option sets a specific tab width for your code (off by default)
"indent": ["warn", 2, {
"SwitchCase": 1,
"MemberExpression": 1,
"FunctionDeclaration": {"parameters": "first"},
"FunctionExpression": {"parameters": "first"},
"CallExpression": {"arguments": 1}
}], // this option sets a specific tab width for your code (off by default)
"brace-style": ["warn", "1tbs", { "allowSingleLine": true }], // enforce one true brace style (off by default)
"key-spacing": ["warn", {"beforeColon": false, "afterColon": true}], // enforces spacing between keys and values in object literal properties
"quotes": ["warn", "single", "avoid-escape"], // specify whether double or single quotes should be used
"keyword-spacing": ["warn", {"before": true, "after": true}], // require a space after certain keywords (off by default)
"no-underscore-dangle": [0],
"import/no-extraneous-dependencies": [0], // because broken
"import/no-unresolved": [0], // because broken
"import/extensions": [0],
"import/prefer-default-export": [0],
"global-require": [0],
"linebreak-style": "off",
// React
//"computed-property-spacing": ["warn", "never"], // disallow spaces inside computed properties
"react/jsx-indent-props": ["warn", 2]
"react/jsx-indent-props": ["warn", 2],
"react/forbid-prop-types": [0],
"react/no-unescaped-entities": [0],
"react/no-unused-prop-types": [0], // because broken
"react/sort-comp": [1, {
"order": [
"static-methods",
"lifecycle",
"/^on.+$/",
"everything-else",
"/^render.+$/",
"render"
]
}]
}
}

View file

@ -1,3 +1,4 @@
/* eslint-disable import/imports-first */
import jquery from 'jquery';
global.$ = jquery;
global.jQuery = jquery;

View file

@ -12,8 +12,8 @@
"copy-mocks": "node scripts/copy-mocks.js",
"renderer": "node ./renderer/renderer.js",
"clean": "./node_modules/.bin/rimraf ./build integration/main.css integration/static && echo --- Cleaned ---",
"lint": "eslint src",
"lint-fix": "eslint src --fix",
"lint": "eslint --ext .js --ext .jsx src webpack stories",
"lint-fix": "npm run lint -- --fix",
"test": "jest --coverage",
"test-watch": "jest --watch",
"e2e-test": "./node_modules/.bin/nightwatch",
@ -77,7 +77,7 @@
"@kadira/storybook-addon-knobs": "^1.4.1",
"babel-cli": "<6.3.0",
"babel-core": "<6.3.0",
"babel-eslint": "^6.0.0",
"babel-eslint": "^7.1.0",
"babel-loader": "~6.2.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-es2015-modules-umd": "^6.8.0",
@ -88,11 +88,11 @@
"copy-webpack-plugin": "^2.1.3",
"css-loader": "^0.23.0",
"enzyme": "^2.3.0",
"eslint": "^2.5.3",
"eslint-config-airbnb": "^6.2.0",
"eslint-plugin-import": "^1.10.2",
"eslint-plugin-jsx-a11y": "^1.5.5",
"eslint-plugin-react": "^4.2.3",
"eslint": "^3.9.1",
"eslint-config-airbnb": "^12.0.0",
"eslint-plugin-import": "^1.16.0",
"eslint-plugin-jsx-a11y": "^2.2.3",
"eslint-plugin-react": "^6.5.0",
"extract-text-webpack-plugin": "^0.9.1",
"file-loader": "^0.8.4",
"html-webpack-plugin": "^2.15.0",

View file

@ -1,27 +1,28 @@
/* eslint-disable jsx-a11y/href-no-hash */
import React from 'react';
import paths from 'constants/paths';
import { Link } from 'react-router';
const Footer = () => (
<footer className="footer">
<div className="col-md-1"></div>
<div className="col-md-3">
<small>
<a href="#">Crédits</a>
<footer className="footer">
<div className="col-md-1" />
<div className="col-md-3">
<small>
<a href="#">Crédits</a>
|
<a href="#">Mentions Légales</a>
</small></div>
<div className="col-md-7">
<nav>
<ul>
<li><Link to={paths.about}>À propos</Link></li>
<li><Link to={paths.contact}>Contact</Link></li>
<li><Link to={paths.external.twitter} target="_blank">Twitter</Link></li>
</ul>
</nav>
</div>
<div className="col-md-1"></div>
</footer>
<a href="#">Mentions Légales</a>
</small></div>
<div className="col-md-7">
<nav>
<ul>
<li><Link to={paths.about}>À propos</Link></li>
<li><Link to={paths.contact}>Contact</Link></li>
<li><Link to={paths.external.twitter} target="_blank">Twitter</Link></li>
</ul>
</nav>
</div>
<div className="col-md-1" />
</footer>
);
export default Footer;

View file

@ -1,23 +1,25 @@
/* eslint-disable jsx-a11y/href-no-hash */
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import paths from 'constants/paths';
const ConnectedUserMenu = ({ user, onLogOut }) => (
<li className="dropdown">
<a href="#" className="dropdown-toggle" data-toggle="dropdown">
{user.name}
<b className="caret"></b>
</a>
<ul className="dropdown-menu">
<li><Link to={paths.getFor.user(user)}>Profil</Link></li>
<li className="divider"></li>
<li><a onClick={onLogOut}>Déconnexion</a></li>
</ul>
</li>
<li className="dropdown">
<a href="#" className="dropdown-toggle" data-toggle="dropdown">
{user.name}
<b className="caret" />
</a>
<ul className="dropdown-menu">
<li><Link to={paths.getFor.user(user)}>Profil</Link></li>
<li className="divider" />
<li><a href="#" onClick={onLogOut}>Déconnexion</a></li>
</ul>
</li>
);
ConnectedUserMenu.propTypes = {
user: PropTypes.object.isRequired,
onLogOut: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
onLogOut: PropTypes.func.isRequired,
};
export default ConnectedUserMenu;

View file

@ -4,18 +4,19 @@ import cssModules from 'react-css-modules';
import { Navbar, Nav, NavItem } from 'react-bootstrap';
import { LinkContainer, IndexLinkContainer } from 'react-router-bootstrap';
import paths from 'constants/paths';
import ConnectedUserMenu from './';
import paths from 'constants/paths';
import logoImg from './images/logo_header.png';
import styles from './Header.css';
const renderUserMenu = (isConnected) => (
const renderUserMenu = isConnected => (
isConnected
? <ConnectedUserMenu />
: [
<li><a>Connexion</a></li>, /* modal ! */
<li><a>Inscription</a></li>, /* modal ! */
<li><a>Connexion</a></li>, /* modal ! */
<li><a>Inscription</a></li>, /* modal ! */
]
);
@ -25,7 +26,7 @@ const Header = () => (
<Navbar.Brand>
<IndexLinkContainer to="/" styleName="logo">
<NavItem>
<img src={logoImg} />
<img alt="Brand Logo" src={logoImg} />
</NavItem>
</IndexLinkContainer>
</Navbar.Brand>

View file

@ -1,5 +1,6 @@
/* eslint-disable no-unused-vars */
import React from 'react';
// eslint-disable-next-line no-unused-vars
import { storiesOf, action, linkTo } from '@kadira/storybook';
import { withKnobs, text, boolean, number } from '@kadira/storybook-addon-knobs';
@ -10,9 +11,9 @@ const stories = storiesOf('Header', module);
stories.addDecorator(withKnobs);
stories.addWithInfo(
'default header',
'Description of the story',
() => (
<Header />
'default header',
'Description of the story',
() => (
<Header />
)
);

View file

@ -5,18 +5,18 @@ import Footer from './Footer';
import GoogleAnalytics from './GoogleAnalytics';
const Main = ({ children }) => (
<div className="container-fluid">
<Header />
<FlashMessages />
<div className="main-content">
{children}
</div>
<Footer />
<GoogleAnalytics />
<div className="container-fluid">
<Header />
<FlashMessages />
<div className="main-content">
{children}
</div>
<Footer />
<GoogleAnalytics />
</div>
);
Main.propTypes = {
children: PropTypes.node,
children: PropTypes.node,
};
export default Main;

View file

@ -1,3 +1,5 @@
/* eslint-disable no-unused-vars */
import { curry } from 'ramda';
import axios from 'axios';
import Config from 'Config';
@ -13,4 +15,3 @@ export const getStatements = () => get('statements');
export const getPublicFiguresAutocomplete = typed => get(`autocomplete/public_figure/${typed}`);
export const getSubjectsAutocomplete = typed => get(`autocomplete/subject/${typed}`);
export const getPositions = subjectId => get(`subjects/${subjectId}/positions`);

View file

@ -14,45 +14,45 @@ const getId = prop('id');
const getAttributesPair = objectWithAttributes => toPairs(objectWithAttributes.attributes);
const mergeAttributes = entityWithAttributes => compose(
reduce(
(entity, attributePair) => (assoc(attributePair[0], attributePair[1], entity)),
entityWithAttributes
reduce(
(entity, attributePair) => (assoc(attributePair[0], attributePair[1], entity)),
entityWithAttributes
),
getAttributesPair,
getAttributesPair,
)(entityWithAttributes);
const overAttributes = over(lensProp('attributes'));
const toCamelCase = replace(
/-[a-z]/g,
compose(replace('-', ''), toUpper)
/-[a-z]/g,
compose(replace('-', ''), toUpper)
);
const toCamelCaseAttributes = overAttributes(pipe(
toPairs,
map(
([k, v]) => ([toCamelCase(k), v])
toPairs,
map(
([k, v]) => ([toCamelCase(k), v])
),
fromPairs,
fromPairs,
));
export const flattenAttributes = pipe(
map(pipe(
toCamelCaseAttributes,
mergeAttributes,
dissoc('attributes'),
map(pipe(
toCamelCaseAttributes,
mergeAttributes,
dissoc('attributes'),
)),
);
export const index = pipe(
when(isNotArray, of),
flattenAttributes,
map(indexBy(getId))
when(isNotArray, of),
flattenAttributes,
map(indexBy(getId))
);
export const indexAndGroup = pipe(
when(isNotArray, of),
flattenAttributes,
groupBy(getType),
map(indexBy(getId))
when(isNotArray, of),
flattenAttributes,
groupBy(getType),
map(indexBy(getId))
);

View file

@ -1,5 +1,5 @@
require('bootstrap-loader');
import moment from 'moment';
import 'bootstrap-loader';
const locale = 'fr';
@ -16,7 +16,8 @@ moment.locale(locale, {
// Install ImmutableDevTools
if (process.env.NODE_ENV !== 'production') { // DEBUG/DEV MODE
const Immutable = require('immutable');
const installDevTools = require('immutable-devtools').default;
installDevTools(Immutable);
const Immutable = require('immutable');
const installDevTools = require('immutable-devtools').default;
installDevTools(Immutable);
}

View file

@ -1,29 +1,29 @@
import React, { PropTypes, Component } from 'react';
import React, { Component } from 'react';
import { Button } from 'react-bootstrap';
import AddStatementModal from 'components/AddStatementModal';
class AddStatementButton extends Component {
state = {
showModal: false,
}
state = {
showModal: false,
}
close = () => this.setState({ showModal: false });
open = () => this.setState({ showModal: true });
close = () => this.setState({ showModal: false });
open = () => this.setState({ showModal: true });
render() {
return (
<div>
<Button
bsStyle="primary"
bsSize="xsmall"
onClick={this.open}
>
render() {
return (
<div>
<Button
bsStyle="primary"
bsSize="xsmall"
onClick={this.open}
>
Nouvelle prise de position
</Button>
<AddStatementModal show={this.state.showModal} onHide={this.close} />
</div>
);
}
<AddStatementModal show={this.state.showModal} onHide={this.close} />
</div>
);
}
}
export default AddStatementButton;

View file

@ -5,6 +5,7 @@ import AddSubjectFrom from './AddSubjectForm';
const SubjectStep = ({ selected, onSelection }) => (
<Well>
<<<<<<< HEAD
<FormGroup controlId="subjectSelect" validationState={selected && !selected.customOption ? 'success' : undefined}>
<ControlLabel>Quel est le sujet qui fait débat ?</ControlLabel>
{(!selected || !selected.customOption) &&
@ -16,6 +17,11 @@ const SubjectStep = ({ selected, onSelection }) => (
onChange={onSelection}
onCancel={() => onSelection(null)}
/>}
=======
<FormGroup controlId="subjectSelect" validationState={!!selected ? 'success' : undefined}>
<ControlLabel>Quel est le sujet qui fait débat ?</ControlLabel>
<SubjectAutocompleteInput selected={selected} onSelection={onSelection} />
>>>>>>> master
</FormGroup>
</Well>
);

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import {
getAddStatementPublicFigure, getAddStatementSubject, getAddStatementPosition,
getAddStatementEvidenceUrl, getAddStatementEvidenceFile
getAddStatementEvidenceUrl, getAddStatementEvidenceFile,
} from 'store/selectors';
import {
onAddStatementPublicFigureSelection, onAddStatementSubjectSelection, onAddStatementPositionSelection,
@ -10,20 +10,20 @@ import {
} from 'store/actions';
const mapStateToProps = state => ({
selectedPublicFigure: getAddStatementPublicFigure(state),
selectedSubject: getAddStatementSubject(state),
selectedPosition: getAddStatementPosition(state),
evidenceUrl: getAddStatementEvidenceUrl(state),
evidenceFile: getAddStatementEvidenceFile(state),
selectedPublicFigure: getAddStatementPublicFigure(state),
selectedSubject: getAddStatementSubject(state),
selectedPosition: getAddStatementPosition(state),
evidenceUrl: getAddStatementEvidenceUrl(state),
evidenceFile: getAddStatementEvidenceFile(state),
});
const mapDispatchToProps = dispatch => ({
onPublicFigureSelection: publicFigureId => dispatch(onAddStatementPublicFigureSelection(publicFigureId)),
onSubjectSelection: publicFigureId => dispatch(onAddStatementSubjectSelection(publicFigureId)),
onPositionSelection: publicFigureId => dispatch(onAddStatementPositionSelection(publicFigureId)),
onUpdateEvidenceUrl: url => dispatch(onAddStatementUpdateEvidenceUrl(url)),
onUpdateEvidenceFile: file => dispatch(onAddStatementUpdateEvidenceFile(file)),
onValidate: () => dispatch(onAddStatementValidate()),
onPublicFigureSelection: publicFigureId => dispatch(onAddStatementPublicFigureSelection(publicFigureId)),
onSubjectSelection: publicFigureId => dispatch(onAddStatementSubjectSelection(publicFigureId)),
onPositionSelection: publicFigureId => dispatch(onAddStatementPositionSelection(publicFigureId)),
onUpdateEvidenceUrl: url => dispatch(onAddStatementUpdateEvidenceUrl(url)),
onUpdateEvidenceFile: file => dispatch(onAddStatementUpdateEvidenceFile(file)),
onValidate: () => dispatch(onAddStatementValidate()),
});
export default connect(mapStateToProps, mapDispatchToProps);

View file

@ -1,20 +1,20 @@
import React, { PropTypes } from 'react';
const getText = nb => {
if (nb === 0)
return "Aucun sujet actif";
else if (nb > 1)
return nb + "sujets actifs";
const getText = (nb) => {
if (nb === 0)
return 'Aucun sujet actif';
else if (nb > 1)
return `${nb}sujets actifs`;
return "1 sujet actif";
return '1 sujet actif';
};
const AssociatedSubjects = ({ publicFigure }) => (
<h6 style={{ color: "#f21e40 !important" }}>{getText(publicFigure.nbActiveSubjects)}</h6>
<h6 style={{ color: '#f21e40 !important' }}>{getText(publicFigure.nbActiveSubjects)}</h6>
);
AssociatedSubjects.propTypes = {
publicFigure: PropTypes.object.isRequired
publicFigure: PropTypes.object.isRequired,
};
export default AssociatedSubjects;
export default AssociatedSubjects;

View file

@ -5,7 +5,7 @@ import React, { PropTypes } from 'react';
const Identity = ({ children }) => <div>{children}</div>;
Identity.propTypes = {
children: PropTypes.node,
children: PropTypes.node,
};
export default Identity;

View file

@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import React from 'react';
import { storiesOf, action, linkTo } from '@kadira/storybook';
import Identity from './';

View file

@ -6,28 +6,28 @@ import cssModules from 'react-css-modules';
import style from './Statement.css';
const Statement = ({ statement }) => (
<li id={`statement-${statement.id}`} styleName="wrapper" >
<PublicFigureAvatar publicFigure={statement.publicFigure} />
<div styleName="public-figure-text">
<strong>
<Link to={paths.getFor.publicFigure(statement.publicFigure)}>
{statement.publicFigure.name}
</Link>
</strong>
<li id={`statement-${statement.id}`} styleName="wrapper" >
<PublicFigureAvatar publicFigure={statement.publicFigure} />
<div styleName="public-figure-text">
<strong>
<Link to={paths.getFor.publicFigure(statement.publicFigure)}>
{statement.publicFigure.name}
</Link>
</strong>
&nbsp; s'est déclaré pour
"<strong>{statement.position.title}</strong>"
dans le débat sur
<strong><Link to={paths.getFor.subject(statement.subject)}>
{statement.subject.title}
{statement.subject.title}
</Link></strong>
</div>
</li>
</div>
</li>
);
Statement.propTypes = {
statement: PropTypes.shape({
publicFigure: PropTypes.object,
position: PropTypes.object,
}).isRequired,
statement: PropTypes.shape({
publicFigure: PropTypes.object,
position: PropTypes.object,
}).isRequired,
};
export default cssModules(Statement, style);

View file

@ -3,11 +3,11 @@ import { getLatestStatements } from 'store/selectors';
import { onLastStatementsAccess } from 'store/actions/entities';
const mapStateToProps = state => ({
statements: getLatestStatements(state),
statements: getLatestStatements(state),
});
const mapDispatchToProps = dispatch => ({
onAccess: () => dispatch(onLastStatementsAccess()),
onAccess: () => dispatch(onLastStatementsAccess()),
});
export default connect(mapStateToProps, mapDispatchToProps);

View file

@ -1,35 +1,35 @@
import React, { PropTypes, Component } from 'react';
import CSSModules from 'react-css-modules';
import cssModules from 'react-css-modules';
import Statement from './Statement';
import LastStatementsStyle from './LastStatements.css';
import connect from './connector';
class LastStatements extends Component {
static propTypes = {
statements: PropTypes.arrayOf(PropTypes.object),
onAccess: PropTypes.func.isRequired,
};
static propTypes = {
statements: PropTypes.arrayOf(PropTypes.object),
onAccess: PropTypes.func.isRequired,
};
componentWillMount() {
this.props.onAccess();
}
componentWillMount() {
this.props.onAccess();
}
renderStatements = () => this.props.statements.map(
(s, i) => <Statement key={i} statement={s} />
renderStatements = () => this.props.statements.map(
(s, i) => <Statement key={i} statement={s} />
);
render() {
if (!this.props.statements) return <span>loading last statements ...</span>;
render() {
if (!this.props.statements) return <span>loading last statements ...</span>;
return (
<div styleName="wrapper">
<h2 styleName="title">Les dernières prises de positions</h2>
<ul styleName="wrapper">
{ this.renderStatements() };
</ul>
</div>
);
}
return (
<div styleName="wrapper">
<h2 styleName="title">Les dernières prises de positions</h2>
<ul styleName="wrapper">
{ this.renderStatements() };
</ul>
</div>
);
}
}
export default connect(CSSModules(LastStatements, LastStatementsStyle));
export default connect(cssModules(LastStatements, LastStatementsStyle));

View file

@ -1,8 +1,8 @@
import React, { Component, PropTypes } from 'react';
import { head, of, compose, when, prop, not, isNil } from 'ramda';
import React, {Component, PropTypes} from 'react';
import {head, of, compose, when, prop, not, isNil} from 'ramda';
import Typeahead from 'react-bootstrap-typeahead';
import { getPublicFiguresAutocomplete } from 'api/debats';
import { flattenAttributes } from 'api/jsonApiParser';
import {getPublicFiguresAutocomplete} from 'api/debats';
import {flattenAttributes} from 'api/jsonApiParser';
import PublicFigureAvatar from 'components/PublicFigureAvatar';
import { makeCancelable } from 'helpers/promises';

View file

@ -3,17 +3,17 @@ import cssModules from 'react-css-modules';
import styles from './PublicFigureAvatar.css';
const PublicFigureAvatar = ({ publicFigure }) => (
<div
styleName="wrapper"
style={{
backgroundImage: !!publicFigure.picture
<div
styleName="wrapper"
style={{
backgroundImage: publicFigure.picture
? `url(${publicFigure.picture.url})`
: undefined,
}}
/>
}}
/>
);
PublicFigureAvatar.propTypes = {
publicFigure: PropTypes.object.isRequired,
publicFigure: PropTypes.object.isRequired,
};
export default cssModules(PublicFigureAvatar, styles);

View file

@ -1,5 +1,6 @@
/* eslint-disable no-unused-vars */
import React from 'react';
// eslint-disable-next-line no-unused-vars
import { storiesOf, action, linkTo } from '@kadira/storybook';
import { withKnobs, text, boolean, number } from '@kadira/storybook-addon-knobs';
import withReadme from 'storybook-readme/with-readme';
@ -12,15 +13,15 @@ const stories = storiesOf('PublicFigureAvatar', module);
stories.addDecorator(withKnobs);
stories.addWithInfo(
'pony avatar',
'Description of the story',
withReadme(README,
() => (
<PublicFigureAvatar publicFigure={{ picture: { url: text('image url', 'http://tinyurl.com/jucz8b9') } }} />
'pony avatar',
'Description of the story',
withReadme(README,
() => (
<PublicFigureAvatar publicFigure={{ picture: { url: text('image url', 'http://tinyurl.com/jucz8b9') } }} />
)
)
);
stories.add('jake avatar', () => (
<PublicFigureAvatar publicFigure={{ picture: { url: 'http://tinyurl.com/jb286jq' } }} />
<PublicFigureAvatar publicFigure={{ picture: { url: 'http://tinyurl.com/jb286jq' } }} />
));

View file

@ -13,7 +13,6 @@ const parseSubjects = raw => pipe(
)
)(raw.data);
class SubjectAutocompleteInput extends Component {
static propTypes = {
@ -57,23 +56,41 @@ class SubjectAutocompleteInput extends Component {
onSelection = compose(this.props.onSelection, head);
render() {
return (
<Typeahead
name="subject"
options={this.state.suggestions}
selected={of(this.props.selected)}
emptyLabel="Aucun sujet correspondante"
labelKey="title"
minLength={3}
allowNew
newSelectionPrefix="Ajouter "
onChange={this.onSelection}
onInputChange={this.loadSuggestions}
renderMenuItemChildren={this.renderMenuItemChildren}
/>
);
}
loadSuggestions = (typed) => {
if (this.props.selected) this.props.onSelection(null);
if (typed.length) {
getSubjectsAutocomplete(typed)
.then((response) => {
this.setState({
suggestions: flattenAttributes(response.data.data),
});
}); }
};
renderMenuItemChildren = (typeaheadProps, subject) => (
<div>
<p>{subject.title} </p>
<small>{take(100)(subject.presentation)}</small>
</div>
);
render() {
return (
<Typeahead
name="subject"
options={this.state.suggestions}
selected={of(this.props.selected)}
emptyLabel="Aucun sujet correspondante"
labelKey="title"
minLength={3}
allowNew
newSelectionPrefix="Ajouter "
onChange={this.onSelection}
onInputChange={this.loadSuggestions}
renderMenuItemChildren={this.renderMenuItemChildren}
/>
);
}
}

View file

@ -1,4 +0,0 @@
import React from 'react';
const Empty = () => <span />;
export default Empty;

View file

@ -1,15 +1,15 @@
export default {
root: '/',
subjects: '/s',
publicFigures: '/p',
getFor: {
subject: (s) => `/s/${s.slug}`,
publicFigure: (pf) => `/p/${pf.slug}`,
},
manual: '/guide',
about: '/about',
contact: '/contact',
external: {
twitter: 'https://twitter.com/debatsco',
},
root: '/',
subjects: '/s',
publicFigures: '/p',
getFor: {
subject: s => `/s/${s.slug}`,
publicFigure: pf => `/p/${pf.slug}`,
},
manual: '/guide',
about: '/about',
contact: '/contact',
external: {
twitter: 'https://twitter.com/debatsco',
},
};

View file

@ -1,14 +1,14 @@
import { pipe, always } from 'ramda';
import { pipe } from 'ramda';
const print = message => x => {
if (!!message) console.warn(message);
return x;
const print = message => (x) => {
if (message) console.warn(message);
return x;
};
const printWithPrefix = (prefix) => (x) => {
console.warn('-------------------------------');
console.warn(`---- ${prefix}: `, x);
return x;
const printWithPrefix = prefix => (x) => {
console.warn('-------------------------------');
console.warn(`---- ${prefix}: `, x);
return x;
};
const printBefore = printWithPrefix('BEFORE');
@ -16,5 +16,5 @@ const printAfter = printWithPrefix('AFTER');
export const withConsole = (f, message) => pipe(printBefore, print(message), f, printAfter);
export const log = x => { console.log(x); return x; }; /* eslint no-console: 0 */
export const warn = x => { console.warn(x); return x; }; /* eslint no-console: 0 */
export const log = (x) => { console.log(x); return x; }; /* eslint no-console: 0 */
export const warn = (x) => { console.warn(x); return x; }; /* eslint no-console: 0 */

View file

@ -1,4 +1,5 @@
import { complement } from 'ramda';
export const hasWindow = () => (typeof window !== 'undefined');
export const isClientSide = hasWindow;
export const isServerSide = complement(hasWindow);

View file

@ -1,9 +1,6 @@
import 'react-hot-loader/patch'; // https://github.com/gaearon/redux-devtools/commit/64f58b7010a1b2a71ad16716eb37ac1031f93915
import 'babel-polyfill';
// First boot side effects
import './boot.js';
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
@ -11,6 +8,10 @@ import { match } from 'react-router';
// Styles
import 'styles/_main.css';
// First boot side effects
import './boot.js';
// Redux
import { store, history } from './store'; // Redux store
import Root from './root'; // App root (Router, Provider, Hot reload ...
@ -18,31 +19,31 @@ import routes from './routes'; // React-router routes
// Init app function
const loadApplication = (DOMElementId) => {
const DOMElement = document.getElementById(DOMElementId);
const DOMElement = document.getElementById(DOMElementId);
match({ history, routes, location }, () => {
ReactDOM.render(
<AppContainer>
<Root store={store} history={history} />
</AppContainer>,
DOMElement
match({ history, routes }, () => {
ReactDOM.render(
<AppContainer>
<Root store={store} history={history} />
</AppContainer>,
DOMElement
);
});
});
if (module.hot) {
module.hot.accept('./root/index.jsx', () => {
if (module.hot) {
module.hot.accept('./root/index.jsx', () => {
// If you use Webpack 2 in ES modules mode, you can
// use <App /> here rather than require() a <NextApp />.
const NextApp = require('./root/index').default;
const NextApp = require('./root/index').default;
ReactDOM.render(
<AppContainer>
<NextApp store={store} history={history} />
</AppContainer>,
DOMElement
);
});
}
ReactDOM.render(
<AppContainer>
<NextApp store={store} history={history} />
</AppContainer>,
DOMElement
);
});
}
};
export { store, history };

View file

@ -3,31 +3,31 @@ import cssModules from 'react-css-modules';
import styles from './About.css';
const About = () => (
<div>
<h1>Pourquoi Débats.co ?</h1>
<p>
<i>Débats</i> est un projet francophone et participatif, ayant pour objectif doffrir une synthèse ouverte, impartiale et vérifiable, des sujets clivants de notre société.
</p>
<p>
Dans un monde de plus en plus complexe, les discours simplistes sont légion. Régulièrement, des controverses se construisent sur la base d'arguments à peine vérifiés, ou simplement invérifiables.
Cest ainsi que semblent se développer des idées confuses et des discussions stériles.
</p>
<p>
L'intelligibilité du débat public et son accessibilité par tous ceux qui aspirent à y participer déterminent la qualité de notre système démocratique. Or, aujourdhui, aucun outil, simple dutilisation, ne permet daccéder à la pluralité des idées de celles et ceux qui font le choix de sexprimer publiquement. Prendre connaissance de leurs arguments, comprendre ce quils prétendent ou ce quils laissent entendre est un véritable parcours du combattant. Nous cédons ainsi régulièrement à la facilité, en accordant une valeur démesurée à des postures démagogiques ou en laissant à dautres le soin de les produire.
</p>
<p>
Pourtant, les ressources sont , sous nos yeux, elles existent ! Les blogs, les tweets, les journaux télévisés, les discours ou les livres sont un trésor démocratique laissé en jachère, une réserve politique fertile jusquici largement sous-exploitée. Ces traces essaimées tout autour de nous constituent une des clés pour y voir plus clair.
</p>
<p>
Lambition de <i>Débats</i> est simple : recenser, sur chaque grand thème qui fait débat, les prises de position de celles et ceux qui décident de sengager. Cartographier lévolution de leur pensée, de leurs opinions, des combats quils ont menés. Donner à voir la constance, la progression ou lincohérence dun engagement dans le temps.
</p>
<p>
Rédigé par des volontaires sur une plateforme en ligne, et fonctionnant sur le principe du wiki, <i>Débats</i> a pour objectif d'offrir un contenu libre, impartial et vérifiable des prises de position de celles et ceux qui participent aux débats publics.
</p>
<p>
Face à ce que nous considérons comme un dévoiement démocratique, nous partageons une conviction : linformation est notre première arme. Redonner à lengagement la crédibilité et la valeur qui doivent être les siennes : voilà la raison dêtre de <i>Débats</i>.
</p>
</div>
<div>
<h1>Pourquoi Débats.co ?</h1>
<p>
<i>Débats</i> est un projet francophone et participatif, ayant pour objectif doffrir une synthèse ouverte, impartiale et vérifiable, des sujets clivants de notre société.
</p>
<p>
Dans un monde de plus en plus complexe, les discours simplistes sont légion. Régulièrement, des controverses se construisent sur la base d'arguments à peine vérifiés, ou simplement invérifiables.
Cest ainsi que semblent se développer des idées confuses et des discussions stériles.
</p>
<p>
L'intelligibilité du débat public et son accessibilité par tous ceux qui aspirent à y participer déterminent la qualité de notre système démocratique. Or, aujourdhui, aucun outil, simple dutilisation, ne permet daccéder à la pluralité des idées de celles et ceux qui font le choix de sexprimer publiquement. Prendre connaissance de leurs arguments, comprendre ce quils prétendent ou ce quils laissent entendre est un véritable parcours du combattant. Nous cédons ainsi régulièrement à la facilité, en accordant une valeur démesurée à des postures démagogiques ou en laissant à dautres le soin de les produire.
</p>
<p>
Pourtant, les ressources sont , sous nos yeux, elles existent ! Les blogs, les tweets, les journaux télévisés, les discours ou les livres sont un trésor démocratique laissé en jachère, une réserve politique fertile jusquici largement sous-exploitée. Ces traces essaimées tout autour de nous constituent une des clés pour y voir plus clair.
</p>
<p>
Lambition de <i>Débats</i> est simple : recenser, sur chaque grand thème qui fait débat, les prises de position de celles et ceux qui décident de sengager. Cartographier lévolution de leur pensée, de leurs opinions, des combats quils ont menés. Donner à voir la constance, la progression ou lincohérence dun engagement dans le temps.
</p>
<p>
Rédigé par des volontaires sur une plateforme en ligne, et fonctionnant sur le principe du wiki, <i>Débats</i> a pour objectif d'offrir un contenu libre, impartial et vérifiable des prises de position de celles et ceux qui participent aux débats publics.
</p>
<p>
Face à ce que nous considérons comme un dévoiement démocratique, nous partageons une conviction : linformation est notre première arme. Redonner à lengagement la crédibilité et la valeur qui doivent être les siennes : voilà la raison dêtre de <i>Débats</i>.
</p>
</div>
);
export default cssModules(About, styles);

View file

@ -13,12 +13,12 @@ const Contact = () => (
<ul className="list-inline banner-social-buttons">
<li>
<a href="mailto:contact@debats.co" className="btn btn-default btn-lg">
<i className="fa fa-envelope-o fa-fw"></i><span className="network-name">&nbsp;Courriel</span>
<i className="fa fa-envelope-o fa-fw" /><span className="network-name">&nbsp;Courriel</span>
</a>
</li>
<li>
<a href="https://twitter.com/debatsco" className="btn btn-default btn-lg">
<i className="fa fa-twitter fa-fw"></i> <span className="network-name">Twitter</span>
<i className="fa fa-twitter fa-fw" /> <span className="network-name">Twitter</span>
</a>
</li>
</ul>

View file

@ -3,14 +3,14 @@ import cssModules from 'react-css-modules';
import styles from './Guide.css';
const Guide = () => (
<div>
<div className="col-md-1"></div>
<div className="col-md-10">
<h1>Mode d'emploi</h1>
<div>
<div className="col-md-1" />
<div className="col-md-10">
<h1>Mode d'emploi</h1>
<h2>Panel de discussion</h2>
<h2>Panel de discussion</h2>
<p>
<p>
La confiance
La plateforme Débats.co est basée sur la confiance. Cest-à-dire que le contenu est systématiquement considéré comme a priori valide, mais peut être retiré en cas de signalement. Plusieurs motifs peuvent pousser les autres membres à signaler votre contenu :
Le contenu ne respecte pas les règles de rédaction
@ -18,142 +18,142 @@ const Guide = () => (
Le contenu pré-existe autrepart sur la plateforme
</p>
<h2>Comment éviter dêtre signalé ?</h2>
<h2>Comment éviter dêtre signalé ?</h2>
<div className="panel-group" id="accordion">
<div className="panel-group" id="accordion">
<div className="panel panel-default">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapse1">
<div className="panel panel-default">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapse1">
Débats.co, quest-ce que cest ?</a>
</h2>
</div>
<div id="collapse1" className="panel-collapse collapse">
<div className="panel-body">
<p>
</h2>
</div>
<div id="collapse1" className="panel-collapse collapse">
<div className="panel-body">
<p>
Débats.co nest pas un site de débats.
<br />
Débats.co a pour objectif de référencer et cartographier les positions prises par des personnalités publiques au cours des débats qui font controverse en France. Ainsi, les contributeurs ne sont pas appelés à donner leur avis mais plutôt à rechercher, collecter et recenser les positions exprimées sur un sujet particulier.
Nous invitons les contributeurs de Débats.co à lire ce mode demploi et à sy référer en cas de doute lors de lutilisation du site.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="panel panel-default">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapse2">Comment fonctionne Débats.co ?</a>
</h2>
</div>
<div id="collapse2" className="panel-collapse collapse">
<div className="panel-body">
<p>
<div className="panel panel-default">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapse2">Comment fonctionne Débats.co ?</a>
</h2>
</div>
<div id="collapse2" className="panel-collapse collapse">
<div className="panel-body">
<p>
Le premier usage de Débats.co consiste à consulter la plateforme, en explorant les différents sujets enregistrés et le positionnement des personnalités publiques au sein de ces sujets.
Le deuxième usage consiste à contribuer à la plateforme en y apportant le contenu nécessaire pour une synthèse des différents sujets et prises de positions.
<br />
Dans les deux cas, il est important de bien comprendre la structure du site. Sur Débats.co, linformation est articulée autour de plusieurs éléments :
</p>
<ul id="reputation">
<li>Le sujet</li>
<li>La position</li>
<li>La personnalité</li>
<li>La prise de position</li>
<li>Largument</li>
<li>La source</li>
</ul>
<p>
<ul id="reputation">
<li>Le sujet</li>
<li>La position</li>
<li>La personnalité</li>
<li>La prise de position</li>
<li>Largument</li>
<li>La source</li>
</ul>
<p>
Chaque <b>sujet</b> comporte au moins deux <b>positions</b>. Une <b>position</b> peut être "prise" par plusieurs <b>personnalités</b>. A chaque <b>prise de position</b> sont attachés une <b>personnalité</b> et un ensemble d<b>arguments</b>. Pour que la <b>prise de position</b> puisse être recensée, elle doit être justifiée par au moins une <b>source</b>.
<br />
En somme, pour quun <b>sujet</b> puisse être référencé, il est nécessaire d'y recenser au moins une <b>prise de position</b> par au moins une <b>personnalité</b> dans chacune des <b>positions</b>.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="panel panel-default">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapse5">
<div className="panel panel-default">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapse5">
Qu'est-ce qu'une source ?</a>
</h2>
</div>
<div id="collapse5" className="panel-collapse collapse">
<div className="panel-body">
<p>
</h2>
</div>
<div id="collapse5" className="panel-collapse collapse">
<div className="panel-body">
<p>
Une <b>source</b> est un élément permettant dappuyer une prise de position. La source peut être constituée à partir de toute référence audio, vidéo, article, ou livre dont la diffusion est, ou a été, publique.
Il sera demandé au contributeur de retranscrire la citation qui soutient le propos en question, la date à laquelle ce propos a été tenu, et, si possible, de fournir un lien qui permet d'y accéder directement.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="panel panel-default">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapse3">Comment référencer et recenser un contenu de qualité ?</a>
</h2>
</div>
<div id="collapse3" className="panel-collapse collapse">
<div className="panel-body">
<p>
<b>
<div className="panel panel-default">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapse3">Comment référencer et recenser un contenu de qualité ?</a>
</h2>
</div>
<div id="collapse3" className="panel-collapse collapse">
<div className="panel-body">
<p>
<b>
Préexistence d'un contenu :
</b>
<br />
<br />
Tout d'abord, le contributeur peut se demander si le contenu qu'il s'apprête à référencer existe déjà.
Lorsqu'il référence un nouveau sujet, il peut vérifier si celui-ci est déjà traité, ou partiellement traité, dans un autre sujet : auquel cas, il pourra enrichir ou modifier le débat déjà existant.
<br />
Lors du recensement d'une prise de position, l'exercice est similaire : il pourra vérifier la pré-existence d'une prise de position pour la personnalité en question, et l'enrichir avec de nouvelles sources le cas échant. Les arguments, matérialisés sur Débats.co par des tags, peuvent être infinis. Le formulaire vous suggérera néanmoins les tags proposés par d'autres utilisateurs.
<br />
<br />
<b>
<br />
<b>
Notoriété et véracité :
</b>
<br />
<br />
Il est nécessaire pour le contributeur de se demander si le contenu est suffisamment important, fiable et pertinent pour être référencé ou recensé sur Débats.co. Basé sur une logique collaborative, le site comporte plusieurs mécanismes d'autorégulation, permettant déviter la publication de contenu considérés comme fallacieux ou hors-sujet. Chaque entrée, notamment lors du référencement dune nouvelle personnalité ou dun nouveau sujet, sont soumis à l'approbation des utilisateurs expérimentés de la plateforme.
<br />
<br />
<br />
<b>Neutralité et cohérence du point de vue</b>
<br />
<b>Neutralité et cohérence du point de vue</b>
<br />
Parler de "cohérence" lorsque l'on tente de référencer des sujets controversées est presque en soi contradictoire. Débats.co a néanmoins comme volonté de tendre un maximum vers l'objectivité.
<br />
Afin de s'en rapprocher, il est nécessaire pour chaque utilisateur de se demander comment exprimer de façon objective lintitulé des sujets ou les positions des personnalités. Dans un premier temps, et pour simplifier cette démarche "objective", les sujets ainsi que les pages de personnalités peuvent être référencées directement depuis la base de données de Wikipedia. Il suffira donc, après la composition des premières lettres de l'intitulé de la personnalité ou du sujet, de sélectionner l'élément correspondant.
<br />
Lors de la recension d'une prise d'une position ensuite, il est important d'attacher à la source une citation qui justifie la prise de position. La citation doit être isolée, entre guillemets, ne pas être modifiée, et se suffir à elle-même pour être comprise (ne pas être sortie d'un contexte qui peut altérer sa compréhensioin). Pour être valide dans le cadre dun sujet, un argument doit être explicitement mobilisé par la personnalité publique et répondre à la problématique initiale du débat.
<br />
</p>
</div>
</div>
</div>
</p>
</div>
</div>
</div>
<div className="panel panel-default">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapse4">Comment avoir accès à toutes les fonctions de Débats.co ?</a>
</h2>
</div>
<div id="collapse4" className="panel-collapse collapse">
<div className="panel-body">
<p>
<div className="panel panel-default">
<div className="panel-heading">
<h2 className="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapse4">Comment avoir accès à toutes les fonctions de Débats.co ?</a>
</h2>
</div>
<div id="collapse4" className="panel-collapse collapse">
<div className="panel-body">
<p>
La contribution au site est simple. Toutefois, avant de pouvoir accéder à toutes les fonctions de référencement, de recension et de modification, une phase dapprentissage est nécessaire. Pour apprendre, il faut sexercer. Ainsi, pour chaque contribution au site, un nombre de points est attribué à lutilisateur. Laccumulation de ces points lui donne progressivement accès aux fonctionnalités les plus puissantes de Débats.co. Le signalement par d'autres utilisateurs de la violation des principes inscrits dans ce mode d'emploi peut entraîner la perte de points. Ainsi 5 status existent sur Débats.co :
</p>
<ul id="reputation">
<li><b>Le Métèque</b> : Son score est neutre. Il vient d'arriver sur la plateforme, et doit encore faire ses preuves. Il peut soumettre des arguments et des sources. </li>
<li><b>L'Eloquent</b> : Son score est positif, il peut ajouter de nouvelles personnalités, de nouveau sujets, mais également suggérer l'approbation ou le rejet de modifications proposées par d'autres utilisateurs. Par défaut, tous les membres fondateurs inscrits lors de la bêta de Débats.co sont considérés comme Eloquents.</li>
<li><b>L'Idéaliste</b> : Son score est élevé, il peut instantanément supprimer la plupart des modifications faites par d'autres utilisateurs.</li>
<li><b>Le Sophiste</b> : son score est négatif, il a tout autant de droits que le nouvel arrivant, mais part avec autant de points de retard pour redevenir Métèque. </li>
</ul>
</div>
</div>
</div>
<ul id="reputation">
<li><b>Le Métèque</b> : Son score est neutre. Il vient d'arriver sur la plateforme, et doit encore faire ses preuves. Il peut soumettre des arguments et des sources. </li>
<li><b>L'Eloquent</b> : Son score est positif, il peut ajouter de nouvelles personnalités, de nouveau sujets, mais également suggérer l'approbation ou le rejet de modifications proposées par d'autres utilisateurs. Par défaut, tous les membres fondateurs inscrits lors de la bêta de Débats.co sont considérés comme Eloquents.</li>
<li><b>L'Idéaliste</b> : Son score est élevé, il peut instantanément supprimer la plupart des modifications faites par d'autres utilisateurs.</li>
<li><b>Le Sophiste</b> : son score est négatif, il a tout autant de droits que le nouvel arrivant, mais part avec autant de points de retard pour redevenir Métèque. </li>
</ul>
</div>
</div>
</div>
<div className="col-md-1"></div>
</div>
</div>
<div className="col-md-1" />
</div>
);
export default cssModules(Guide, styles);

View file

@ -6,40 +6,40 @@ import PublicFigureAvatar from 'components/PublicFigureAvatar';
const PublicFigureAvatarMapper = pf => <PublicFigureAvatar key={pf.id} publicFigure={pf} />;
const renderAssociatedPublicFigures = compose(
map(PublicFigureAvatarMapper),
take(5),
prop('remarquablePublicFigures'),
map(PublicFigureAvatarMapper),
take(5),
prop('remarquablePublicFigures'),
);
const HomeSubject = ({ subject }) => (
<li>
<tr>
<td style={{ width: '50%', border: 'none', textTransform: 'uppercase' }}>
<h2 className="subjects-title">
<Link to={paths.getFor.subject(subject)}>
{subject.title}
</Link>
</h2>
<h6 className="count">
{`${subject.remarquablePublicFigures.length} personnalité(s)`}
</h6>
</td>
<td style={{ width: '50%', textAlign: 'center', verticalAlign: 'middle' }}>
{renderAssociatedPublicFigures(subject)}
</td>
<td className="seemore">
<div>
<Link to={paths.getFor.subject(subject)}>Voir plus de personnalités</Link>
</div>
</td>
</tr>
</li>
<li>
<tr>
<td style={{ width: '50%', border: 'none', textTransform: 'uppercase' }}>
<h2 className="subjects-title">
<Link to={paths.getFor.subject(subject)}>
{subject.title}
</Link>
</h2>
<h6 className="count">
{`${subject.remarquablePublicFigures.length} personnalité(s)`}
</h6>
</td>
<td style={{ width: '50%', textAlign: 'center', verticalAlign: 'middle' }}>
{renderAssociatedPublicFigures(subject)}
</td>
<td className="seemore">
<div>
<Link to={paths.getFor.subject(subject)}>Voir plus de personnalités</Link>
</div>
</td>
</tr>
</li>
);
HomeSubject.propTypes = {
subject: PropTypes.shape({
title: PropTypes.string.isRequired,
remarquablePublicFigures: PropTypes.arrayOf(PropTypes.object).isRequired,
}).isRequired,
subject: PropTypes.shape({
title: PropTypes.string.isRequired,
remarquablePublicFigures: PropTypes.arrayOf(PropTypes.object).isRequired,
}).isRequired,
};
export default (HomeSubject);

View file

@ -3,11 +3,11 @@ import { getHomeSubjectsWithRelations } from 'store/selectors';
import { onHottestSubjectsAccess } from 'store/actions/entities';
const mapStateToProps = state => ({
subjects: getHomeSubjectsWithRelations(state),
subjects: getHomeSubjectsWithRelations(state),
});
const mapDispatchToProps = dispatch => ({
onAccess: () => dispatch(onHottestSubjectsAccess()),
onAccess: () => dispatch(onHottestSubjectsAccess()),
});
export default connect(mapStateToProps, mapDispatchToProps);

View file

@ -4,26 +4,26 @@ import connector from './connectors';
class HomeSubjects extends Component {
static propTypes = {
subjects: PropTypes.arrayOf(PropTypes.object).isRequired,
onAccess: PropTypes.func.isRequired,
}
static propTypes = {
subjects: PropTypes.arrayOf(PropTypes.object).isRequired,
onAccess: PropTypes.func.isRequired,
}
componentWillMount() {
this.props.onAccess();
}
componentWillMount() {
this.props.onAccess();
}
render() {
if (!this.props.subjects) return <span>loading subjects ...</span>;
render() {
if (!this.props.subjects) return <span>loading subjects ...</span>;
return (
<div> {/* TODO Bootstrap */}
{this.props.subjects.map(
s => <HomeSubject key={s.id} subject={s} />
return (
<div> {/* TODO Bootstrap */}
{this.props.subjects.map(
s => <HomeSubject key={s.id} subject={s} />
)}
</div>
);
}
</div>
);
}
}

View file

@ -1,37 +1,37 @@
import React from 'react';
import cssModules from 'react-css-modules';
import HomeSubjects from './HomeSubjects';
import LastStatements from 'components/LastStatements';
import HomeSubjects from './HomeSubjects';
import bgSrc from './images/intro-bg.jpg';
import styles from './Home.css';
const Home = () => (
<div className="container-fluid" styleName="container">
<div className="row" styleName="background-title" style={{ backgroundImage: `url(${bgSrc})` }}>
<div styleName="title-mask">
<h5 >Bienvenue sur Débats.co</h5>
<div styleName="introduction">
<span>Débats est un projet francophone et participatif, ayant pour objectif </span>
<span>doffrir une synthèse ouverte, impartiale et vérifiable, des sujets clivants de notre société.</span>
</div>
</div>
<div className="container-fluid" styleName="container">
<div className="row" styleName="background-title" style={{ backgroundImage: `url(${bgSrc})` }}>
<div styleName="title-mask">
<h5 >Bienvenue sur Débats.co</h5>
<div styleName="introduction">
<span>Débats est un projet francophone et participatif, ayant pour objectif </span>
<span>doffrir une synthèse ouverte, impartiale et vérifiable, des sujets clivants de notre société.</span>
</div>
<div className="col-md-1"></div>
<div className="subjects-index col-md-7 subjects-home">
<h1>Sujets d'actualité</h1>
<table className="table">
<tbody>
<ul id="subject">
<HomeSubjects />
</ul>
</tbody>
</table>
</div>
<div className="col-md-3 col-centered" style={{ textAlign: 'right' }} >
<LastStatements />
</div>
<div className="col-md-1"></div>
</div>
</div>
<div className="col-md-1" />
<div className="subjects-index col-md-7 subjects-home">
<h1>Sujets d'actualité</h1>
<table className="table">
<tbody>
<ul id="subject">
<HomeSubjects />
</ul>
</tbody>
</table>
</div>
<div className="col-md-3 col-centered" style={{ textAlign: 'right' }} >
<LastStatements />
</div>
<div className="col-md-1" />
</div>
);
export default cssModules(Home, styles);

View file

@ -1,55 +1,47 @@
import React, { PropTypes } from 'react';
import paths from '../../../constants/paths';
import cssModules from 'react-css-modules';
import { Link } from 'react-router';
import paths from 'constants/paths';
import PublicFigureAvatar from 'components/PublicFigureAvatar';
import AssociatedSubjects from 'components/AssociatedSubjects';
import PublicFigureStyle from './PublicFigure.css';
import cssModules from 'react-css-modules';
const PublicFigureInList = ({ publicFigure }) => (
<table className="table">
<tbody>
<ul id="subject">
<li>
<table className="table">
<tbody>
<ul id="subject">
<li>
<tr>
<td>
<PublicFigureAvatar publicFigure={publicFigure} />
</td>
<td style= {{ width: '33%' }}>
<tr>
<td>
<PublicFigureAvatar publicFigure={publicFigure} />
</td>
<td style={{ width: '33%' }}>
<h2 className="figure-title" style={{ color: '#333333 !important' }}>
<Link to={paths.getFor.subject(publicFigure)}>
{publicFigure.name}
</Link>
</h2>
<AssociatedSubjects publicFigure={publicFigure} />
<h2 className="figure-title" style={{ color: '#333333 !important' }}>
<Link to={paths.getFor.subject(publicFigure)}>
{publicFigure.name}
</Link>
</h2>
<AssociatedSubjects publicFigure={publicFigure} />
</td>
<td styleName="presentationWrapper">
<p className="figure-presentation-text" styleName="presentation">
{publicFigure.presentation}
</p>
<p className="figure-presentation-text" styleName="presentation">
{publicFigure.presentation}
</p>
</td>
</tr>
</tr>
</li>
</ul>
</tbody>
</table>
</ul>
</tbody>
</table>
);
PublicFigureInList.propTypes = {
publicFigure: PropTypes.object.isRequired,
publicFigure: PropTypes.object.isRequired,
};
export default cssModules(PublicFigureInList, PublicFigureStyle);
/*
<script type="text/javascript">
$(document).ready(function() {
$(".figure-presentation-text").shorten();
});
</script>
*/

View file

@ -3,11 +3,11 @@ import { getPublicFiguresWithRelations } from 'store/selectors';
import { onPublicFiguresListAccess } from 'store/actions/entities';
const mapStateToProps = state => ({
publicFigures: getPublicFiguresWithRelations(state),
publicFigures: getPublicFiguresWithRelations(state),
});
const mapDispatchToProps = dispatch => ({
onAccess: () => dispatch(onPublicFiguresListAccess()),
onAccess: () => dispatch(onPublicFiguresListAccess()),
});
export default connect(mapStateToProps, mapDispatchToProps);

View file

@ -1,37 +1,38 @@
import React, { PropTypes, Component } from 'react';
import LastStatements from 'components/LastStatements';
import PublicFigureInList from './PublicFigureInList';
import connect from './connector';
import LastStatements from 'components/LastStatements';
class PublicFigures extends Component {
static propTypes = {
publicFigures: PropTypes.arrayOf(PropTypes.object).isRequired,
onAccess: PropTypes.func.isRequired,
}
static propTypes = {
publicFigures: PropTypes.arrayOf(PropTypes.object).isRequired,
onAccess: PropTypes.func.isRequired,
}
componentWillMount() {
this.props.onAccess();
}
componentWillMount() {
this.props.onAccess();
}
render() {
if (!this.props.publicFigures) return <span>loading public figures ...</span>;
render() {
if (!this.props.publicFigures) return <span>loading public figures ...</span>;
const renderChilds = () => this.props.publicFigures.map(
pf => <PublicFigureInList key={pf.id} publicFigure={pf} />
const renderChilds = () => this.props.publicFigures.map(
pf => <PublicFigureInList key={pf.id} publicFigure={pf} />
);
return (
<div>
<div className="col-md-9">
{renderChilds()}
</div>
<div className="col-md-3">
<LastStatements />
</div>
</div>
);
}
return (
<div>
<div className="col-md-9">
{renderChilds()}
</div>
<div className="col-md-3">
<LastStatements />
</div>
</div>
);
}
}
export default connect(PublicFigures);

View file

@ -4,14 +4,14 @@ import { Router } from 'react-router';
import routes from '../routes';
const Root = ({ store, history }) => (
<Provider store={store}>
<Router history={history} routes={routes} />
</Provider>
<Provider store={store}>
<Router history={history} routes={routes} />
</Provider>
);
Root.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
};
export default Root;

View file

@ -1,81 +0,0 @@
import React from 'react';
import { Route, IndexRoute } from 'react-router';
// Main Page
import Main from 'Main/index';
export default (
<Route path="/" component={Main}>
<IndexRoute
getComponent={(nextState, done) => {
require.ensure([], require => {
done(null, require('pages/Home').default);
}, 'home');
}}
/>
<Route
path="contact"
name="contact"
getComponent={(nextState, done) => {
require.ensure([], require => {
done(null, require('pages/Contact').default);
}, 'contact');
}}
/>
<Route
path="about"
name="about"
getComponent={(nextState, done) => {
require.ensure([], require => {
done(null, require('pages/About').default);
}, 'about');
}}
/>
<Route
path="guide"
name="guide"
getComponent={(nextState, done) => {
require.ensure([], require => {
done(null, require('pages/Guide').default);
}, 'guide');
}}
/>
<Route path="s" name="subjects">
<IndexRoute
name="subjects.index"
getComponent={(nextState, done) => {
require.ensure([], require => {
done(null, require('pages/Subjects').default);
}, 'subjects.index');
}}
/>
<Route
name="subjects.page"
path=":subjectSlug"
getComponent={(nextState, done) => {
require.ensure([], require => {
done(null, require('pages/Subject').default);
}, 'subjects.page');
}}
/>
</Route>
<Route path="p" name="publicFigures">
<IndexRoute
name="publicFigures.index"
getComponent={(nextState, done) => {
require.ensure([], require => {
done(null, require('pages/PublicFigures').default);
}, 'publicFigures.index');
}}
/>
<Route
name="publicFigures.page"
path=":publicFigureSlug"
getComponent={(nextState, done) => {
require.ensure([], require => {
done(null, require('pages/publicFigure').default);
}, 'publicFigures.page');
}}
/>
</Route>
</Route>
);

82
src/routes/index.jsx Normal file
View file

@ -0,0 +1,82 @@
import React from 'react';
import { Route, IndexRoute } from 'react-router';
// Main Page
import Main from 'Main/index';
export default (
<Route path="/" component={Main}>
<IndexRoute
getComponent={(nextState, done) => {
require.ensure([], (require) => {
done(null, require('pages/Home').default);
}, 'home');
}}
/>
<Route
path="contact"
name="contact"
getComponent={(nextState, done) => {
require.ensure([], (require) => {
done(null, require('pages/Contact').default);
}, 'contact');
}}
/>
<Route
path="about"
name="about"
getComponent={(nextState, done) => {
require.ensure([], (require) => {
done(null, require('pages/About').default);
}, 'about');
}}
/>
<Route
path="guide"
name="guide"
getComponent={(nextState, done) => {
require.ensure([], (require) => {
done(null, require('pages/Guide').default);
}, 'guide');
}}
/>
<Route path="s" name="subjects">
<IndexRoute
name="subjects.index"
getComponent={(nextState, done) => {
require.ensure([], (require) => {
done(null, require('pages/Subjects').default);
}, 'subjects.index');
}}
/>
<Route
name="subjects.page"
path=":subjectSlug"
getComponent={(nextState, done) => {
require.ensure([], (require) => {
done(null, require('pages/Subject').default);
}, 'subjects.page');
}}
/>
</Route>
<Route path="p" name="publicFigures">
<IndexRoute
name="publicFigures.index"
getComponent={(nextState, done) => {
require.ensure([], (require) => {
done(null, require('pages/PublicFigures').default);
}, 'publicFigures.index');
}}
/>
<Route
name="publicFigures.page"
path=":publicFigureSlug"
getComponent={(nextState, done) => {
require.ensure([], (require) => {
done(null, require('pages/publicFigure').default);
}, 'publicFigures.page');
}}
/>
</Route>
</Route>
);

View file

@ -1,27 +1,27 @@
import types from '../actions_types';
export const onAddStatementPublicFigureSelection = (id) => ({
type: types.ADD_STATEMENT_PUBLIC_FIGURE_SELECTION,
id
export const onAddStatementPublicFigureSelection = id => ({
type: types.ADD_STATEMENT_PUBLIC_FIGURE_SELECTION,
id,
});
export const onAddStatementSubjectSelection = (id) => ({
type: types.ADD_STATEMENT_SUBJECT_SELECTION,
id
export const onAddStatementSubjectSelection = id => ({
type: types.ADD_STATEMENT_SUBJECT_SELECTION,
id,
});
export const onAddStatementPositionSelection = (id) => ({
type: types.ADD_STATEMENT_POSITION_SELECTION,
id
export const onAddStatementPositionSelection = id => ({
type: types.ADD_STATEMENT_POSITION_SELECTION,
id,
});
export const onAddStatementUpdateEvidenceUrl = (url) => ({
type: types.ADD_STATEMENT_UPDATE_EVIDENCE_URL,
url
export const onAddStatementUpdateEvidenceUrl = url => ({
type: types.ADD_STATEMENT_UPDATE_EVIDENCE_URL,
url,
});
export const onAddStatementUpdateEvidenceFile = (file) => ({
type: types.ADD_STATEMENT_UPDATE_EVIDENCE_FILE,
file
export const onAddStatementUpdateEvidenceFile = file => ({
type: types.ADD_STATEMENT_UPDATE_EVIDENCE_FILE,
file,
});
export const onAddStatementValidate = () => ({
type: types.ADD_STATEMENT_VALIDATE,
type: types.ADD_STATEMENT_VALIDATE,
});

View file

@ -1,22 +1,22 @@
import actionsTypes from '../actions_types';
export const onLastStatementsAccess = () => ({
type: actionsTypes.ENTITY_ACCESS,
accessType: 'list',
entityType: 'statements',
listType: 'latest',
type: actionsTypes.ENTITY_ACCESS,
accessType: 'list',
entityType: 'statements',
listType: 'latest',
});
export const onHottestSubjectsAccess = () => ({
type: actionsTypes.ENTITY_ACCESS,
accessType: 'list',
entityType: 'subjects',
listType: 'hottest',
type: actionsTypes.ENTITY_ACCESS,
accessType: 'list',
entityType: 'subjects',
listType: 'hottest',
});
export const onPublicFiguresListAccess = () => ({
type: actionsTypes.ENTITY_ACCESS,
accessType: 'list',
entityType: 'publicFigures',
listType: 'all',
type: actionsTypes.ENTITY_ACCESS,
accessType: 'list',
entityType: 'publicFigures',
listType: 'all',
});

View file

@ -1,11 +1,11 @@
export default {
ENTITY_ACCESS: 'ENTITY_ACCESS',
ENTITY_READ: 'ENTITY_READ',
ADD_STATEMENT_NEXT_STEP: 'ADD_STATEMENT_NEXT_STEP',
ADD_STATEMENT_PUBLIC_FIGURE_SELECTION: 'ADD_STATEMENT_PUBLIC_FIGURE_SELECTION',
ADD_STATEMENT_SUBJECT_SELECTION: 'ADD_STATEMENT_SUBJECT_SELECTION',
ADD_STATEMENT_POSITION_SELECTION: 'ADD_STATEMENT_POSITION_SELECTION',
ADD_STATEMENT_UPDATE_EVIDENCE_URL: 'ADD_STATEMENT_UPDATE_EVIDENCE_URL',
ADD_STATEMENT_UPDATE_EVIDENCE_FILE: 'ADD_STATEMENT_UPDATE_EVIDENCE_FILE',
ADD_STATEMENT_VALIDATE: 'ADD_STATEMENT_PUBLIC_FIGURE_SELECTION',
ENTITY_ACCESS: 'ENTITY_ACCESS',
ENTITY_READ: 'ENTITY_READ',
ADD_STATEMENT_NEXT_STEP: 'ADD_STATEMENT_NEXT_STEP',
ADD_STATEMENT_PUBLIC_FIGURE_SELECTION: 'ADD_STATEMENT_PUBLIC_FIGURE_SELECTION',
ADD_STATEMENT_SUBJECT_SELECTION: 'ADD_STATEMENT_SUBJECT_SELECTION',
ADD_STATEMENT_POSITION_SELECTION: 'ADD_STATEMENT_POSITION_SELECTION',
ADD_STATEMENT_UPDATE_EVIDENCE_URL: 'ADD_STATEMENT_UPDATE_EVIDENCE_URL',
ADD_STATEMENT_UPDATE_EVIDENCE_FILE: 'ADD_STATEMENT_UPDATE_EVIDENCE_FILE',
ADD_STATEMENT_VALIDATE: 'ADD_STATEMENT_VALIDATE',
};

View file

@ -1,16 +1,15 @@
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { routerReducer, routerMiddleware } from 'react-router-redux';
import { routerReducer, routerMiddleware, syncHistoryWithStore } from 'react-router-redux';
import { map } from 'ramda';
import { isClientSide } from 'helpers/env';
import rootSaga from './sagas';
import { entitiesReducer, addStatementReducer } from './reducers';
import { useRouterHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import createHashHistory from 'history/lib/createHashHistory';
import createMemoryHistory from 'history/lib/createMemoryHistory';
import rootSaga from './sagas';
import { entitiesReducer, addStatementReducer } from './reducers';
// Build history
const createHistory = isClientSide() ? createHashHistory : createMemoryHistory;
// const browserHistory = useScroll(useRouterHistory(createHistory))();
@ -21,31 +20,31 @@ const initialState = isClientSide() ? window.__INITIAL_STATE__ : {};
// REDUCERS
const reducer = combineReducers({
routing: routerReducer,
entities: entitiesReducer,
addStatement: addStatementReducer,
routing: routerReducer,
entities: entitiesReducer,
addStatement: addStatementReducer,
});
// MIDDLEWARE
const sagaMiddleware = createSagaMiddleware();
const middlewares = [routerMiddleware(browserHistory), sagaMiddleware];
if (isClientSide() && process.env.NODE_ENV !== 'production') {
middlewares.push(require('redux-logger')({
stateTransformer: map(state => ((state && state.toJS) ? state.toJS() : state)),
timestamp: true,
duration: true,
collapsed: true,
predicate: (getState, action) => action.type !== 'APP_LOG',
}));
middlewares.push(require('redux-logger')({
stateTransformer: map(state => ((state && state.toJS) ? state.toJS() : state)),
timestamp: true,
duration: true,
collapsed: true,
predicate: (getState, action) => action.type !== 'APP_LOG',
}));
}
// STORE
const store = createStore(
reducer,
initialState,
compose(
applyMiddleware(...middlewares),
window.devToolsExtension ? window.devToolsExtension() : f => f,
reducer,
initialState,
compose(
applyMiddleware(...middlewares),
window.devToolsExtension ? window.devToolsExtension() : f => f,
)
);
const history = syncHistoryWithStore(browserHistory, store);

View file

@ -4,42 +4,43 @@ import {
import actionsTypes from '../actions_types';
const initialState = {
publicFigureId: null,
subjectId: null,
positionId: null,
statementDate: null,
evidenceUrl: null,
evidenceFile: null,
quote: null,
note: null,
tags: [],
publicFigureId: null,
subjectId: null,
positionId: null,
statementDate: null,
evidenceUrl: null,
evidenceFile: null,
quote: null,
note: null,
tags: [],
};
const isPublicFigureChosen = compose(not, isNil, prop('publicFigureId'));
const isSubjectChosen = compose(not, isNil, prop('publicFigureId'));
const isPositionChosen = compose(not, isNil, prop('publicFigureId'));
const isStatementComplete = allPass([
compose(not, isNil, prop('statementDate')),
compose(not, isNil, prop('quote')),
compose(not, isNil, prop('statementDate')),
either(
compose(not, isNil, prop('evidenceUrl')),
compose(not, isNil, prop('evidenceFile')),
)
compose(not, isNil, prop('statementDate')),
compose(not, isNil, prop('quote')),
compose(not, isNil, prop('statementDate')),
either(
compose(not, isNil, prop('evidenceUrl')),
compose(not, isNil, prop('evidenceFile')),
),
]);
/* eslint-disable no-unused-vars */
const isPublicFigureMissing = complement(isPublicFigureChosen);
const isSubjectMissing = complement(isSubjectChosen);
const isPositionMissing = complement(isPositionChosen);
const isStatementIncomplete = complement(isStatementComplete);
export const addStatementReducer = (state = initialState, action) => {
switch (action.type) {
case actionsTypes.ADD_STATEMENT_PUBLIC_FIGURE_SELECTION: return assoc('publicFigureId', action.id, state);
case actionsTypes.ADD_STATEMENT_SUBJECT_SELECTION: return assoc('subjectId', action.id, state);
case actionsTypes.ADD_STATEMENT_POSITION_SELECTION: return assoc('positionId', action.id, state);
case actionsTypes.ADD_STATEMENT_UPDATE_EVIDENCE_URL: return assoc('evidenceUrl', action.url, state);
case actionsTypes.ADD_STATEMENT_UPDATE_EVIDENCE_FILE: return assoc('evidenceFile', action.file, state);
default: return state;
}
switch (action.type) {
case actionsTypes.ADD_STATEMENT_PUBLIC_FIGURE_SELECTION: return assoc('publicFigureId', action.id, state);
case actionsTypes.ADD_STATEMENT_SUBJECT_SELECTION: return assoc('subjectId', action.id, state);
case actionsTypes.ADD_STATEMENT_POSITION_SELECTION: return assoc('positionId', action.id, state);
case actionsTypes.ADD_STATEMENT_UPDATE_EVIDENCE_URL: return assoc('evidenceUrl', action.url, state);
case actionsTypes.ADD_STATEMENT_UPDATE_EVIDENCE_FILE: return assoc('evidenceFile', action.file, state);
default: return state;
}
};

View file

@ -1,35 +1,36 @@
import actionsType from '../actions_types';
import { map, compose, prop, merge, assoc, not, isNil, pipe, when } from 'ramda';
const initialState = {};
import { indexAndGroup } from 'api/jsonApiParser';
import actionsType from '../actions_types';
const initialState = {};
const isNotNil = compose(not, isNil);
export const indexAndGroupMainData = compose(
indexAndGroup,
map(assoc('fetched', true)),
prop('data')
indexAndGroup,
map(assoc('fetched', true)),
prop('data')
);
const indexAndGroupIncludedData = pipe(
prop('included'),
when(
isNotNil,
compose(
indexAndGroup,
map(assoc('fetched', true)),
prop('included'),
when(
isNotNil,
compose(
indexAndGroup,
map(assoc('fetched', true)),
)
),
);
const saveData = data => compose(
merge(indexAndGroupIncludedData(data)),
indexAndGroupMainData,
merge(indexAndGroupIncludedData(data)),
indexAndGroupMainData,
)(data);
export const entitiesReducer = (state = initialState, action) => {
switch (action.type) {
case actionsType.ENTITY_READ: return merge(state, saveData(action.data));
default: return state;
}
switch (action.type) {
case actionsType.ENTITY_READ: return merge(state, saveData(action.data));
default: return state;
}
};

View file

@ -1,59 +1,59 @@
import { identity } from 'ramda';
import actionsTypes from '../actions_types';
import { takeEvery } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
import { getSubjects, getStatements, getPositions } from 'api/debats';
import actionsTypes from '../actions_types';
const getApiCallFor = (entityType, entityRequest) => {
switch (entityType) {
case 'statements': return getStatements;
case 'subjects':
switch (entityRequest) {
case 'hottest': return getSubjects;
default: return identity;
}
case 'publicFigures':
switch (entityRequest) {
case 'hottest': return getSubjects;
case 'all': return getSubjects;
default: return identity;
}
switch (entityType) {
case 'statements': return getStatements;
case 'subjects':
switch (entityRequest) {
case 'hottest': return getSubjects;
default: return identity;
}
}
case 'publicFigures':
switch (entityRequest) {
case 'hottest': return getSubjects;
case 'all': return getSubjects;
default: return identity;
}
default: return identity;
}
};
function* fetchEntityIfNeeded(action) {
// Test Do we have to call API
const apiCall = getApiCallFor(action.entityType, (
action.accessType === 'list' ? action.listType : action.accessedId
const apiCall = getApiCallFor(action.entityType, (
action.accessType === 'list' ? action.listType : action.accessedId
));
// Call API
const response = yield call(apiCall);
const response = yield call(apiCall);
// Error actions
// Success actions
yield put({ type: actionsTypes.ENTITY_READ, data: response.data });
yield put({ type: actionsTypes.ENTITY_READ, data: response.data });
}
export function* watchEntityAccess() {
yield* takeEvery(actionsTypes.ENTITY_ACCESS, fetchEntityIfNeeded);
yield* takeEvery(actionsTypes.ENTITY_ACCESS, fetchEntityIfNeeded);
}
function* fetchPositionsOfSubject(action) {
if (!!action.id) {
if (action.id) {
// Call API
const response = yield call(getPositions, action.id);
const response = yield call(getPositions, action.id);
// Error actions
// Success actions
yield put({ type: actionsTypes.ENTITY_READ, data: response.data });
}
yield put({ type: actionsTypes.ENTITY_READ, data: response.data });
}
}
export function* watchSubjectSelection() {
yield* takeEvery(actionsTypes.ADD_STATEMENT_SUBJECT_SELECTION, fetchPositionsOfSubject);
yield* takeEvery(actionsTypes.ADD_STATEMENT_SUBJECT_SELECTION, fetchPositionsOfSubject);
}

View file

@ -2,8 +2,8 @@ import { watchEntityAccess, watchSubjectSelection } from './apiSaga';
export default function* rootSaga() {
yield [
watchEntityAccess(),
watchSubjectSelection()
];
yield [
watchEntityAccess(),
watchSubjectSelection(),
];
}

View file

@ -2,8 +2,6 @@ import { createSelector } from 'reselect';
import { prop, propEq, find, pipe, dissoc, when, not, isNil, compose } from 'ramda';
import { getPublicFigures, getSubjects, getPositions, enrichWithRelationships } from './entities';
import { withConsole } from 'helpers/debug';
const isNotNil = compose(not, isNil);
const getAddState = state => state.addStatement;
@ -11,43 +9,43 @@ const getAddState = state => state.addStatement;
const injectPositions = enrichWithRelationships('positions', 'positions');
export const getAddStatementPublicFigure = createSelector(
getAddState,
getPublicFigures,
(addState, publicFigures) => find(
propEq('id', prop('publicFigureId')(addState))
)(publicFigures)
getAddState,
getPublicFigures,
(addState, publicFigures) => find(
propEq('id', prop('publicFigureId')(addState))
)(publicFigures)
);
export const getAddStatementSubject = createSelector(
getAddState,
getSubjects,
getPositions,
(addState, allSubjects, allPositions) => pipe(
find(propEq('id', prop('subjectId')(addState))),
when(
isNotNil,
pipe(
injectPositions(allPositions),
dissoc('relationthips')
)
),
)(allSubjects)
getAddState,
getSubjects,
getPositions,
(addState, allSubjects, allPositions) => pipe(
find(propEq('id', prop('subjectId')(addState))),
when(
isNotNil,
pipe(
injectPositions(allPositions),
dissoc('relationthips')
)
),
)(allSubjects)
);
export const getAddStatementPosition = createSelector(
getAddState,
getPositions,
(addState, publicFigures) => find(
propEq('id', prop('positionId')(addState))
)(publicFigures)
getAddState,
getPositions,
(addState, publicFigures) => find(
propEq('id', prop('positionId')(addState))
)(publicFigures)
);
export const getAddStatementEvidenceUrl = createSelector(
getAddState,
prop('evidenceUrl')
getAddState,
prop('evidenceUrl')
);
export const getAddStatementEvidenceFile = createSelector(
getAddState,
prop('evidenceFile')
getAddState,
prop('evidenceFile')
);

View file

@ -1,18 +1,18 @@
import { curry, assoc, compose, when, take, path, is, map, isNil, always, values, propEq, ifElse, pipe, find } from 'ramda';
export const getSubjects = state => values(state.entities.subjects);
export const getPositions = state => values(state.entities.positions);
export const getStatements = state => values(state.entities.statements);
export const getPublicFigures = state => values(state.entities['public-figures']);
import { withConsole, warn } from 'helpers/debug';
const getEntityByRef = curry(
(sourceEntities, entityReference) => ifElse(
isNil,
always(entityReference), // No source entities yet, return reference object
pipe(
find(propEq('id', entityReference.id)),
when(isNil, always(entityReference)), // Entity not fetched yet, return reference object
(sourceEntities, entityReference) => ifElse(
isNil,
always(entityReference), // No source entities yet, return reference object
pipe(
find(propEq('id', entityReference.id)),
when(isNil, always(entityReference)), // Entity not fetched yet, return reference object
),
)(sourceEntities)
);
@ -23,14 +23,14 @@ const getEntityByRef = curry(
* const injectPublicFigure = enrichWithRelationship('publicFigure', 'public-figure');
*/
export const enrichWithRelationship = curry(
(propertyName, relationshipName, fromCollection, entity) => assoc(
propertyName,
compose(
getEntityByRef(fromCollection),
when(is(Array), take(1)),
path(['relationships', relationshipName, 'data']),
(propertyName, relationshipName, fromCollection, entity) => assoc(
propertyName,
compose(
getEntityByRef(fromCollection),
when(is(Array), take(1)),
path(['relationships', relationshipName, 'data']),
)(entity),
entity
entity
)
);
@ -41,13 +41,13 @@ export const enrichWithRelationship = curry(
* = enrichWithRelationships('remarquablePublicFigures', 'remarquable-public-figures');
*/
export const enrichWithRelationships = curry(
(propertyName, relationshipName, fromCollection, entity) => assoc(
propertyName,
compose(
map(getEntityByRef(fromCollection)),
path(['relationships', relationshipName, 'data'])
(propertyName, relationshipName, fromCollection, entity) => assoc(
propertyName,
compose(
map(getEntityByRef(fromCollection)),
path(['relationships', relationshipName, 'data'])
)(entity),
entity
entity
)
);

View file

@ -1,4 +1,6 @@
/* eslint-disable import/export */ // since positions.js is an empty file
export * from './positions';
export * from './publicFigures';
export * from './statements';
export * from './subjects';

View file

@ -1,4 +1,6 @@
import { values, pipe, compose, map, dissoc, prop } from 'ramda';
/* eslint-disable no-unused-vars */
import { values, pipe, compose, map, dissoc } from 'ramda';
import { whenNotNil } from 'helpers/ramda-ext';
import { createSelector } from 'reselect';
import { enrichWithRelationships, getSubjects, getPublicFigures } from './entities';
@ -6,14 +8,14 @@ import { enrichWithRelationships, getSubjects, getPublicFigures } from './entiti
const injectSubjects = enrichWithRelationships('subjects', 'subjects');
export const getPublicFiguresWithRelations = createSelector(
getPublicFigures,
getSubjects,
(publicFigures, allSubjects) => whenNotNil(
compose(
values,
map(pipe(
getPublicFigures,
getSubjects,
(publicFigures, allSubjects) => whenNotNil(
compose(
values,
map(pipe(
// injectSubjects(allSubjects),
dissoc('relationthips'),
dissoc('relationthips'),
))
)
)(publicFigures)

View file

@ -2,7 +2,7 @@ import { values, pipe, map, compose, dissoc } from 'ramda';
import { createSelector } from 'reselect';
import { whenNotNil } from 'helpers/ramda-ext';
import {
enrichWithRelationship, getPublicFigures, getPositions, getSubjects, getStatements
enrichWithRelationship, getPublicFigures, getPositions, getSubjects, getStatements,
} from './entities';
const injectPublicFigure = enrichWithRelationship('publicFigure', 'public-figure');
@ -10,18 +10,18 @@ const injectPosition = enrichWithRelationship('position', 'position');
const injectSubject = enrichWithRelationship('subject', 'subject');
export const getLatestStatements = createSelector(
getStatements,
getPublicFigures,
getPositions,
getSubjects,
(allStatements, allPublicFigures, allPositions, allSubjects) => whenNotNil(
compose(
values,
map(pipe(
injectPublicFigure(allPublicFigures),
injectSubject(allSubjects),
injectPosition(allPositions),
dissoc('relationthips'),
getStatements,
getPublicFigures,
getPositions,
getSubjects,
(allStatements, allPublicFigures, allPositions, allSubjects) => whenNotNil(
compose(
values,
map(pipe(
injectPublicFigure(allPublicFigures),
injectSubject(allSubjects),
injectPosition(allPositions),
dissoc('relationthips'),
))
)
)(allStatements)

View file

@ -1,9 +1,8 @@
import { values, pipe, compose, map, dissoc, propEq } from 'ramda';
import { find, values, pipe, compose, map, dissoc, propEq } from 'ramda';
import { createSelector } from 'reselect';
import { whenNotNil } from 'helpers/ramda-ext';
import { enrichWithRelationships, getPublicFigures, getPositions, getSubjects } from './entities';
import { withConsole } from 'helpers/debug';
import { enrichWithRelationships, getPublicFigures, getPositions, getSubjects } from './entities';
export const getHomeSubjects = state => values(getSubjects(state));
@ -13,29 +12,29 @@ const injectRemarquablePublicFigures
const injectPositions = enrichWithRelationships('positions', 'positions');
export const getHomeSubjectsWithRelations = createSelector(
getHomeSubjects,
getPublicFigures,
getPositions,
(homeSubjects, allPublicFigures, allPositions) => whenNotNil(
compose(
values,
map(pipe(
injectRemarquablePublicFigures(allPublicFigures),
injectPositions(allPositions),
dissoc('relationships'),
getHomeSubjects,
getPublicFigures,
getPositions,
(homeSubjects, allPublicFigures, allPositions) => whenNotNil(
compose(
values,
map(pipe(
injectRemarquablePublicFigures(allPublicFigures),
injectPositions(allPositions),
dissoc('relationships'),
))
)
)(homeSubjects)
);
export const getSubject = (state, props) => createSelector(
getSubjects,
getPositions,
getPublicFigures,
(allSubjects, allPositions, allPublicFigures) => pipe(
find(propEq('id', props.subjectId)),
injectRemarquablePublicFigures(allPublicFigures),
injectPositions(allPositions),
dissoc('relationthips'),
getSubjects,
getPositions,
getPublicFigures,
(allSubjects, allPositions, allPublicFigures) => pipe(
find(propEq('id', props.subjectId)),
injectRemarquablePublicFigures(allPublicFigures),
injectPositions(allPositions),
dissoc('relationthips'),
)(allSubjects)
);

View file

@ -1,3 +1,3 @@
import { urlRegex } from './generic';
export const isValidEvidenceUrl = (tested) => urlRegex.test(tested);
export const isValidEvidenceUrl = tested => urlRegex.test(tested);

View file

@ -4,11 +4,11 @@ const SRC_FOLDER = 'src';
const APP_ROOT = path.normalize(path.join(__dirname, '..'));
const constants = Object.freeze({
APP_NAME: 'debats',
APP_ROOT,
APP_PATH: path.join(APP_ROOT, SRC_FOLDER),
STATIC_PATH: path.join(APP_ROOT, SRC_FOLDER, 'static'),
SRC_FOLDER,
APP_NAME: 'debats',
APP_ROOT,
APP_PATH: path.join(APP_ROOT, SRC_FOLDER),
STATIC_PATH: path.join(APP_ROOT, SRC_FOLDER, 'static'),
SRC_FOLDER,
});
module.exports = constants;

View file

@ -1,54 +1,55 @@
const CONSTANTS = require('../webpack/constants');
const APP_PATH = CONSTANTS.APP_PATH;
const loaders = [
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /src.+\.css$/,
loader: 'style-loader'
+ '!css-loader'
+ '?modules&localIdentName=[name]__[local]___[hash:base64:5]'
+ '&importLoaders=1'
+ '!postcss-loader',
},
{
test: /node_modules.*\.css$/,
loader: 'style-loader'
+ '!css-loader'
+ '!postcss-loader',
},
{
test: /\.(png|svg|gif|jpeg|jpg)$/,
include: APP_PATH,
loader: 'file?name=images/[name].[ext]',
},
{
test: /\.(eot|woff|ttf)$/,
include: APP_PATH,
loader: 'file-loader?name=fonts/[name].[ext]',
},
{
test: /\.(svg|woff2?)$/,
exclude: APP_PATH,
loader: 'url?limit=10000',
},
{
test: /\.(ttf|eot)$/,
exclude: APP_PATH,
loader: 'file',
},
{
test: /\.json$/,
loader: 'json',
},
{
test: /\.md$/,
loader: 'raw',
},
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /src.+\.css$/,
loader: 'style-loader'
+ '!css-loader'
+ '?modules&localIdentName=[name]__[local]___[hash:base64:5]'
+ '&importLoaders=1'
+ '!postcss-loader',
},
{
test: /node_modules.*\.css$/,
loader: 'style-loader'
+ '!css-loader'
+ '!postcss-loader',
},
{
test: /\.(png|svg|gif|jpeg|jpg)$/,
include: APP_PATH,
loader: 'file?name=images/[name].[ext]',
},
{
test: /\.(eot|woff|ttf)$/,
include: APP_PATH,
loader: 'file-loader?name=fonts/[name].[ext]',
},
{
test: /\.(svg|woff2?)$/,
exclude: APP_PATH,
loader: 'url?limit=10000',
},
{
test: /\.(ttf|eot)$/,
exclude: APP_PATH,
loader: 'file',
},
{
test: /\.json$/,
loader: 'json',
},
{
test: /\.md$/,
loader: 'raw',
},
];
module.exports = loaders;

View file

@ -12,35 +12,36 @@ const postcssHide = require('postcss-hide');
const CONSTANTS = require('./constants');
const makeMap = aPath => {
try {
return webpackPostcssTools.makeVarMap(aPath);
} catch (e) {
const makeMap = (aPath) => {
try {
return webpackPostcssTools.makeVarMap(aPath);
} catch (e) {
// console.log(`${aPath} not found.`);
process.exit(1);
}
return null;
process.exit(1);
}
return null;
};
const map = makeMap('./src/styles/_constants.css');
// eslint-disable-next-line no-unused-vars
module.exports = webpack => [
postcssMixins({
mixinsDir: path.join(CONSTANTS.APP_PATH, 'style', 'mixins'),
}),
atImport(),
postcssShortPosition(),
postcssAssets(),
postcssColorFunction(),
postcssNext({
features: {
customProperties: {
variables: map.vars,
warnings: false,
},
},
}),
lost(),
postcssInlineSVG(),
postcssHide(),
postcssMixins({
mixinsDir: path.join(CONSTANTS.APP_PATH, 'style', 'mixins'),
}),
atImport(),
postcssShortPosition(),
postcssAssets(),
postcssColorFunction(),
postcssNext({
features: {
customProperties: {
variables: map.vars,
warnings: false,
},
},
}),
lost(),
postcssInlineSVG(),
postcssHide(),
];

View file

@ -1,12 +1,12 @@
const path = require('path');
const CONSTANTS = require('./constants');
const APP_ROOT = CONSTANTS.APP_ROOT;
module.exports = {
alias: {
react: path.resolve(APP_ROOT, 'node_modules/react'),
},
modulesDirectories: ['src', 'web_modules', 'node_modules'],
extensions: ['', '.js', '.jsx'],
alias: {
react: path.resolve(APP_ROOT, 'node_modules/react'),
},
modulesDirectories: ['src', 'web_modules', 'node_modules'],
extensions: ['', '.js', '.jsx'],
};

View file

@ -5,6 +5,7 @@ const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CONSTANTS = require('./constants');
const APP_ROOT = CONSTANTS.APP_ROOT;
const SRC_FOLDER = CONSTANTS.SRC_FOLDER;

View file

@ -5,6 +5,7 @@ const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CONSTANTS = require('./constants');
const APP_ROOT = CONSTANTS.APP_ROOT;
const APP_PATH = CONSTANTS.APP_PATH;
const SRC_FOLDER = CONSTANTS.SRC_FOLDER;
@ -21,12 +22,12 @@ config.plugins.push(new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"product
config.plugins.push(new webpack.optimize.DedupePlugin());
config.plugins.push(new webpack.optimize.OccurenceOrderPlugin(true)); // Assign the module and chunk ids by occurrence count
config.plugins.push(new webpack.optimize.UglifyJsPlugin({
compressor: { screw_ie8: false, keep_fnames: true, warnings: true },
mangle: {
except: ['$super', '$', 'exports', 'require'],
screw_ie8: false,
keep_fnames: true,
},
compressor: { screw_ie8: false, keep_fnames: true, warnings: true },
mangle: {
except: ['$super', '$', 'exports', 'require'],
screw_ie8: false,
keep_fnames: true,
},
}));
// config.plugins.push(new CompressionPlugin({threshold: 10240}));
config.plugins.push(new CopyWebpackPlugin(copyConfig));
@ -34,7 +35,7 @@ config.devtool = 'cheap-module-source-map';
config.cache = false;
config.debug = false;
config.entry = `${APP_PATH}/index.js`;
config.output.path = outputPath;;
config.output.path = outputPath;
module.exports = config;

View file

@ -2,20 +2,21 @@ const config = require('./webpack.base.config');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CONSTANTS = require('./constants');
const APP_PATH = CONSTANTS.APP_PATH;
const APP_NAME = CONSTANTS.APP_NAME;
const SRC_FOLDER = CONSTANTS.SRC_FOLDER;
config.plugins.push(new HtmlWebpackPlugin({
template: `${SRC_FOLDER}/index.html`,
inject: true,
filename: 'index.html',
template: `${SRC_FOLDER}/index.html`,
inject: true,
filename: 'index.html',
}));
config.entry = `${APP_PATH}/index.js`;
config.entry = `${APP_PATH}/index.jsx`;
config.output = {
filename: `${APP_NAME}.[name].[hash].js`,
filename: `${APP_NAME}.[name].[hash].js`,
};
module.exports = config;

View file

@ -5,6 +5,7 @@ const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CONSTANTS = require('./constants');
const APP_ROOT = CONSTANTS.APP_ROOT;
const SRC_FOLDER = CONSTANTS.SRC_FOLDER;
@ -33,30 +34,30 @@ var devServerAPIUrl;
var devServerRewrite;
if (args.proxy) {
if (args.mockAPI) {
devServerAPIUrl = 'http://localhost:3030';
devServerRewrite = function rewrite(req) {
req.url = req.url // eslint-disable-line no-param-reassign
if (args.mockAPI) {
devServerAPIUrl = 'http://localhost:3030';
devServerRewrite = function rewrite(req) {
req.url = req.url // eslint-disable-line no-param-reassign
.replace(/\/api\//, '/fake-api/') // Local mocks as API
.split('?')[0]; // strip query string if exists
if (req.method === 'POST') req.method = 'GET'; // eslint-disable-line no-param-reassign
};
}
if (args.prodAPI) devServerAPIUrl = 'http://api.débats.co';
if (req.method === 'POST') req.method = 'GET'; // eslint-disable-line no-param-reassign
};
}
if (args.prodAPI) devServerAPIUrl = 'http://api.débats.co';
}
config.devServer = {
quiet: false,
stats: { colors: true },
outputPath,
proxy: {
'/api/*': {
target: devServerAPIUrl,
rewrite: devServerRewrite,
changeOrigin: true,
secure: false,
},
quiet: false,
stats: { colors: true },
outputPath,
proxy: {
'/api/*': {
target: devServerAPIUrl,
rewrite: devServerRewrite,
changeOrigin: true,
secure: false,
},
},
};
module.exports = config;