...
 
Commits (5)
export const START_TALKING = "START_TALKING";
export const STOP_TALKING = "STOP_TALKING";
export const ALREADY_TALKED = "ALREADY_TALKED";
export const IS_ORGANIZER = "IS_ORGANIZER";
export const TICK = "TICK";
export const CHANGE_TIMER_LIMIT = "CHANGE_TIMER_LIMIT";
function previousTalk(state) {
function currentSpeech(state) {
const current = state.current;
if (current.category === null) {
return null;
}
const now = Math.round(Date.now() / 1000),
duration = now - current.startedAt,
overtime = duration > state.limits.maxTime,
startDate = new Date(current.startedAt * 1000);
return {
timestamp: startDate.toISOString(),
category: current.category,
duration: Date.now() - current.startedAt
duration,
alreadyTalked: current.alreadyTalked,
isOrganizer: current.isOrganizer,
overtime
};
}
......@@ -20,8 +29,8 @@ export function startTalking(category) {
return dispatch({
type: START_TALKING,
category,
timestamp: Date.now(),
previousTalk: previousTalk(getState())
timestamp: Math.round(Date.now() / 1000),
speech: currentSpeech(getState())
});
};
}
......@@ -31,7 +40,7 @@ export function stopTalking() {
const state = getState();
return dispatch({
type: STOP_TALKING,
previousTalk: previousTalk(state)
speech: currentSpeech(state)
});
};
}
......@@ -49,10 +58,23 @@ export function alreadyTalked() {
};
}
export function isOrganizer() {
return (dispatch, getState) => {
const category = getState().current.category;
if (category === null) {
return null;
}
return dispatch({
type: IS_ORGANIZER,
category
});
};
}
export function tick() {
return {
type: TICK,
timestamp: Date.now()
timestamp: Math.round(Date.now() / 1000)
};
}
......
......@@ -13,7 +13,7 @@ const styles = {
const messages = defineMessages({
text: {
id: "alreadytalkedbutton.text",
defaultMessage: "This person has already spoken"
defaultMessage: "Has already spoken"
}
});
......
......@@ -6,6 +6,7 @@ import Typography from "@material-ui/core/Typography";
import { injectIntl, defineMessages, FormattedMessage } from "react-intl";
import ConnectedSpeakerButton from "../containers/ConnectedSpeakerButton";
import ConnectedAlreadyTalkedButton from "../containers/ConnectedAlreadyTalkedButton";
import ConnectedIsOrganizerButton from "../containers/ConnectedIsOrganizerButton";
import ConnectedTimer from "../containers/ConnectedTimer";
import ConnectedTimerLimitEdit from "../containers/ConnectedTimerLimitEdit";
import ConnectedTicker from "../containers/ConnectedTicker";
......@@ -63,25 +64,34 @@ class App extends Component {
<FormattedMessage {...messages.title} />
</Typography>
</Grid>
<Grid item>
<ConnectedSpeakerButton
category="man"
isNew={true}
text={this.props.intl.formatMessage(messages.dude)}
/>
<Grid item xs={6}>
<Typography align="right">
<ConnectedSpeakerButton
category="man"
isNew={true}
text={this.props.intl.formatMessage(messages.dude)}
/>
</Typography>
</Grid>
<Grid item>
<ConnectedSpeakerButton
category="other"
isNew={true}
text={this.props.intl.formatMessage(messages.notdude)}
/>
<Grid item xs={6}>
<Typography align="left">
<ConnectedSpeakerButton
category="other"
isNew={true}
text={this.props.intl.formatMessage(messages.notdude)}
/>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography align="center">
<Grid item xs={6}>
<Typography align="right">
<ConnectedAlreadyTalkedButton />
</Typography>
</Grid>
<Grid item xs={6}>
<Typography align="left">
<ConnectedIsOrganizerButton />
</Typography>
</Grid>
</Grid>
</Grid>
......
......@@ -9,62 +9,59 @@ const messages = defineMessages({
id: "exportbutton.save",
defaultMessage: "Save"
},
date: {
id: "exportbutton.date",
defaultMessage: "Date"
timestamp: {
id: "exportbutton.timestamp",
defaultMessage: "Timestamp"
},
timeMan: {
id: "exportbutton.timeMan",
defaultMessage: "Men speaking time"
category: {
id: "exportbutton.category",
defaultMessage: "Category"
},
timeOther: {
id: "exportbutton.timeOther",
defaultMessage: "Other speaking time"
duration: {
id: "exportbutton.duration",
defaultMessage: "Duration"
},
speechesMan: {
id: "exportbutton.speechesMan",
defaultMessage: "Speeches by men"
alreadyTalked: {
id: "exportbutton.alreadyTalked",
defaultMessage: "Has already talked"
},
speechesOther: {
id: "exportbutton.speechesOther",
defaultMessage: "Other speeches"
isOrganizer: {
id: "exportbutton.isOrganizer",
defaultMessage: "Organizer"
},
speakersMan: {
id: "exportbutton.speakersMan",
defaultMessage: "Number of men speaking"
},
speakersOther: {
id: "exportbutton.speakersOther",
defaultMessage: "Number of other speakers"
overtime: {
id: "exportbutton.overtime",
defaultMessage: "Overtime"
}
});
class ExportButton extends Component {
makeData = () => {
const now = new Date();
const toCSVLine = a => a.map(v => '"' + v + '"').join(",") + ";\n";
let data = [
toCSVLine([
this.props.intl.formatMessage(messages.date),
this.props.intl.formatMessage(messages.timeMan),
this.props.intl.formatMessage(messages.timeOther),
this.props.intl.formatMessage(messages.speechesMan),
this.props.intl.formatMessage(messages.speechesOther),
this.props.intl.formatMessage(messages.speakersMan),
this.props.intl.formatMessage(messages.speakersOther)
]),
toCSVLine([
now.toISOString(),
this.props.timeMan,
this.props.timeOther,
this.props.speechesMan,
this.props.speechesOther,
this.props.speakersMan,
this.props.speakersOther
this.props.intl.formatMessage(messages.timestamp),
this.props.intl.formatMessage(messages.category),
this.props.intl.formatMessage(messages.duration),
this.props.intl.formatMessage(messages.alreadyTalked),
this.props.intl.formatMessage(messages.isOrganizer),
this.props.intl.formatMessage(messages.overtime)
])
];
for (let i = 0; i < this.props.speeches.length; i++) {
const speech = this.props.speeches[i];
data.push(
toCSVLine([
speech.timestamp,
speech.category,
speech.duration,
speech.alreadyTalked,
speech.isOrganizer,
speech.overtime
])
);
}
return data;
//return data.map((v) => ('"' + v + '"')).join(",") + ";";
};
onClick = e => {
......
import React, { Component } from "react";
import { withStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import grey from "@material-ui/core/colors/grey";
import { defineMessages, FormattedMessage } from "react-intl";
const styles = {
button: {
color: grey[600]
}
};
const messages = defineMessages({
text: {
id: "isorganizerbutton.text",
defaultMessage: "Organizer"
}
});
class IsOrganizerButton extends Component {
render() {
const { classes } = this.props;
return (
<Button
variant="text"
color="default"
size="small"
onClick={this.props.onClick}
className={classes.button}
disabled={this.props.disabled}
>
<FormattedMessage {...messages.text} />
</Button>
);
}
}
export default withStyles(styles)(IsOrganizerButton);
......@@ -7,16 +7,14 @@ class SpeakerButton extends Component {
render() {
return (
<div className="SpeakerButton">
<Button
variant={this.props.selected ? "contained" : "outlined"}
color="primary"
size="large"
onClick={this.onClick}
>
{this.props.text}
</Button>
</div>
<Button
variant={this.props.selected ? "contained" : "outlined"}
color="primary"
size="large"
onClick={this.onClick}
>
{this.props.text}
</Button>
);
}
}
......
......@@ -8,7 +8,7 @@ import ListItemText from "@material-ui/core/ListItemText";
import Icon from "@material-ui/core/Icon";
import { injectIntl, defineMessages, FormattedMessage } from "react-intl";
import Duration from "./Duration";
import ExportButton from "./ExportButton";
import ExportButton from "../containers/ConnectedExportButton";
const styles = {
root: {
......@@ -79,10 +79,10 @@ class StatsValues extends Component {
? 0
: Math.round((this.props.stats.man.speakers / totalSpeakers) * 100);
const manMeanTime = Math.round(
this.props.stats.man.duration / this.props.stats.man.speeches / 1000
this.props.stats.man.duration / this.props.stats.man.speeches
);
const otherMeanTime = Math.round(
this.props.stats.other.duration / this.props.stats.other.speeches / 1000
this.props.stats.other.duration / this.props.stats.other.speeches
);
if (totalSpeakers === 0) {
......@@ -108,8 +108,8 @@ class StatsValues extends Component {
})}
secondary={
<span>
(<Duration value={this.props.stats.man.duration / 1000} />/
<Duration value={totalSpokenTime / 1000} />)
(<Duration value={this.props.stats.man.duration} />/
<Duration value={totalSpokenTime} />)
</span>
}
/>
......@@ -170,14 +170,7 @@ class StatsValues extends Component {
</List>
<Typography align="right">
<ExportButton
timeMan={Math.round(this.props.stats.man.duration / 1000)}
timeOther={Math.round(this.props.stats.other.duration / 1000)}
speechesMan={this.props.stats.man.speeches}
speechesOther={this.props.stats.other.speeches}
speakersMan={this.props.stats.man.speakers}
speakersOther={this.props.stats.other.speakers}
/>
<ExportButton />
</Typography>
</div>
);
......
......@@ -32,7 +32,7 @@ const styles = {
class Timer extends Component {
render() {
const { classes } = this.props;
const counter = Math.trunc(this.props.ticker / 1000);
const counter = this.props.ticker;
let color = "default";
let barColor = "primary";
let barClasses = {};
......
......@@ -7,7 +7,9 @@ const mapStateToProps = (state, ownProps) => {
disabled:
state.current.category === null ||
state.current.alreadyTalked ||
state.talked[state.current.category].speakers === 0
state.speeches.filter(
speech => speech.category === state.current.category
).length === 0
};
};
......
import { connect } from "react-redux";
import ExportButton from "../components/ExportButton";
const mapStateToProps = (state, ownProps) => {
return {
speeches: state.speeches
};
};
const ConnectedExportButton = connect(mapStateToProps)(ExportButton);
export default ConnectedExportButton;
import { connect } from "react-redux";
import { isOrganizer } from "../actions";
import IsOrganizerButton from "../components/IsOrganizerButton";
const mapStateToProps = (state, ownProps) => {
return {
disabled: state.current.category === null || state.current.isOrganizer
};
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => dispatch(isOrganizer())
};
};
const ConnectedIsOrganizerButton = connect(
mapStateToProps,
mapDispatchToProps
)(IsOrganizerButton);
export default ConnectedIsOrganizerButton;
import { connect } from "react-redux";
import Stats from "../components/Stats";
const computeDuration = speeches =>
speeches.reduce((total, current) => total + current.duration, 0);
const computeSpeakers = speeches =>
speeches.reduce(
(total, current) => total + (current.alreadyTalked ? 0 : 1),
0
);
const mapStateToProps = (state, ownProps) => {
const manSpeeches = state.speeches.filter(
speech => speech.category === "man"
);
const otherSpeeches = state.speeches.filter(
speech => speech.category === "other"
);
let stats = {
man: {
...state.talked.man
duration: computeDuration(manSpeeches),
speeches: manSpeeches.length,
speakers: computeSpeakers(manSpeeches)
},
other: {
...state.talked.other
duration: computeDuration(otherSpeeches),
speeches: otherSpeeches.length,
speakers: computeSpeakers(otherSpeeches)
}
};
if (state.current.ticker !== null) {
stats[state.current.category].duration += state.current.ticker;
stats[state.current.category].speeches += 1;
stats[state.current.category].speakers += 1;
stats[state.current.category].speakers += state.current.alreadyTalked
? 0
: 1;
}
return { stats };
};
......
{
"alreadytalkedbutton.text": "Cette personne a déjà parlé",
"alreadytalkedbutton.text": "A déjà parlé",
"app.dude": "Un mec",
"app.notdude": "Pas un mec",
"app.title": "Qui parle ?",
"exportbutton.date": "Date",
"exportbutton.alreadyTalked": "A déjà parlé",
"exportbutton.category": "Categorie",
"exportbutton.duration": "Durée",
"exportbutton.isOrganizer": "Organisateur",
"exportbutton.overtime": "A dépassé",
"exportbutton.save": "Sauvegarder",
"exportbutton.speakersMan": "Nombre d'intervenants mecs",
"exportbutton.speakersOther": "Nombre d'autres intervenants",
"exportbutton.speechesMan": "Interventions de mecs",
"exportbutton.speechesOther": "Interventions des autres",
"exportbutton.timeMan": "Temps parlé par des mecs",
"exportbutton.timeOther": "Temps parlé par des autres",
"exportbutton.timestamp": "Horodatage",
"footer.text1": "Très largement inspiré de",
"footer.text2": "En fait, c'est pareil mais avec quelques stats en plus.",
"footer.viewCode": "Voir le code",
"header.title": "Les hommes parlent trop, prouvez-le.",
"isorganizerbutton.text": "Organisateur/trice",
"stats.forOthers": "pour les autres :",
"stats.header": "Les mecs ont parlé :",
"stats.nobody": "Personne n'a encore parlé.",
......@@ -27,4 +27,4 @@
"timerlimitedit.modaltitle": "Temps de parole maximum",
"timerlimitedit.nolimit": "sans limite",
"timerlimitedit.seconds": "Secondes"
}
}
\ No newline at end of file
[
"exportbutton.date",
"timerlimitedit.buttontext",
"timerlimitedit.minutes"
]
\ No newline at end of file
......@@ -3,6 +3,7 @@ import {
START_TALKING,
STOP_TALKING,
ALREADY_TALKED,
IS_ORGANIZER,
TICK,
CHANGE_TIMER_LIMIT
} from "./actions";
......@@ -18,20 +19,10 @@ const initialState = {
ticker: null,
startedAt: null,
category: null,
alreadyTalked: false
alreadyTalked: false,
isOrganizer: false
},
talked: {
man: {
duration: 0,
speeches: 0,
speakers: 0
},
other: {
duration: 0,
speeches: 0,
speakers: 0
}
}
speeches: []
};
function limits(state = initialState.limits, action) {
......@@ -53,7 +44,8 @@ function current(state = initialState.current, action) {
ticker: 0,
startedAt: action.timestamp,
category: action.category,
alreadyTalked: false
alreadyTalked: false,
isOrganizer: false
};
case STOP_TALKING:
return {
......@@ -71,36 +63,24 @@ function current(state = initialState.current, action) {
...state,
alreadyTalked: true
};
case IS_ORGANIZER:
return {
...state,
isOrganizer: true
};
default:
return state;
}
}
function talked(state = initialState.talked, action) {
function speeches(state = initialState.speeches, action) {
switch (action.type) {
case START_TALKING:
case STOP_TALKING:
if (action.previousTalk === null) {
if (action.speech === null) {
return state;
}
const category = action.previousTalk.category,
duration = action.previousTalk.duration;
return {
...state,
[category]: {
speeches: (state[category] ? state[category].speeches : 0) + 1,
speakers: (state[category] ? state[category].speakers : 0) + 1,
duration: (state[category] ? state[category].duration : 0) + duration
}
};
case ALREADY_TALKED:
return {
...state,
[action.category]: {
...state[action.category],
speakers: state[action.category].speakers - 1
}
};
return [...state, action.speech];
default:
return state;
}
......@@ -109,7 +89,7 @@ function talked(state = initialState.talked, action) {
const rootReducer = combineReducers({
limits,
current,
talked
speeches
});
export default rootReducer;