I'm looking for some feedback on a pomodoro clock that I recently wrote with React.
I'm pretty new to React and coding in general. I followed a couple of online courses, but I'm mostly self-taught. My academic background and my actual job have nothing to do with programming, so I really don't know how to rate my work.
A couple of things I have doubts on:
- Is the code clean and easily readable?
- Is there some common practice that I didn't follow?
Of course any other advice or observation would be much appreciated.
Here's the code of the main component, but since I used more modules it would make my day if you could take a look at the github repo.
class App extends React.Component {
constructor() {
super();
this.state = {
// prop used by SetInterval and ClearInterval
timerId: 0,
start: 0,
// this property is used by tick method
startTime: "25:00",
// value displayed by timer
currentTime: "25:00",
workTime: "25",
shortBreakTime: "5",
longBreakTime: "30",
// how many pomodoros before long break
lBDelay: 4,
pomodorosCompleted: 0,
// session is Ready when startTime is updated
sessionReady: "work",
sessionRunning: "",
theme: "violet",
sound: "on",
// to set the correct CSS variables for svg animation
animationWasPaused: false
};
this.notificationDOMRef = React.createRef();
}
componentDidMount() {
let circle = document.querySelector("circle");
circle.style.setProperty("--time", "initial");
}
// handles notification component
addNotification(mess, typ) {
this.notificationDOMRef.current.addNotification({
message: mess,
type: typ,
insert: "top",
container: "top-right",
animationIn: ["animated", "fadeIn"],
animationOut: ["animated", "fadeOut"],
dismiss: { duration: 5000 },
dismissable: { click: true }
});
};
prepareNewSession = () => {
const val = this.state[`${this.state.sessionReady}Time`];
const time = val < 10 ? `0${val}:00` : `${val}:00`;
this.setState({
sessionRunning: "",
startTime: time,
currentTime: time
});
};
handleStart = () => {
const {
sessionRunning,
sessionReady,
startTime,
animationWasPaused
} = this.state;
// disable button if session is already running
if (sessionRunning) {
this.addNotification("session has already started", "warning");
return;
}
// toggle session that is starting now
this.setState({
sessionRunning: sessionReady,
start: Date.now(),
timerId: setInterval(this.setTimer, 1000)
});
// start svg timer
let circle = document.querySelector("circle");
if (animationWasPaused) {
circle.style.setProperty("--pauseHandler", "running");
this.setState({
animationWasPaused: false
});
return;
}
const seconds = `${toSeconds(startTime)}s`;
circle.style.setProperty("--time", seconds);
circle.style.setProperty("--pauseHandler", "running");
};
setTimer = () => {
const {
startTime,
start,
currentTime,
timerId,
sessionReady,
pomodorosCompleted,
lBDelay
} = this.state;
// convert string to number and then to seconds
let duration = toSeconds(startTime);
let display = tick(duration, start);
this.setState({
currentTime: display
});
if (currentTime === "00:00") {
clearInterval(timerId);
this.alarm.play();
// according to session that just finished update props in state and start new session
if (sessionReady === "work") {
// check if we already have n pomodoros completed and need to switch to long break
this.setState({
pomodorosCompleted: pomodorosCompleted + 1
});
const remainder = (pomodorosCompleted + 1) % lBDelay;
remainder === 0 ?
this.handleSelect("longBreak") :
this.handleSelect("shortBreak");
} else {
this.handleSelect("work");
}
// give time to update svg circle CSS var
setTimeout(this.handleStart, 1);
}
};
handleStop = () => {
let {
sessionRunning,
timerId,
currentTime
} = this.state;
if (!sessionRunning) {
this.addNotification("session isn't running", "warning");
return;
}
clearInterval(timerId);
// pause svg timer
const circle = document.querySelector("circle");
circle.style.setProperty("--pauseHandler", "paused");
this.setState({
sessionRunning: "",
startTime: currentTime,
animationWasPaused: true
});
};
handleReset = () => {
const {
sessionRunning,
timerId
} = this.state;
if (!sessionRunning) {
this.addNotification("session isn't running", "warning");
return;
}
clearInterval(timerId);
this.prepareNewSession();
// reset svg timer
let circle = document.querySelector("circle");
circle.style.setProperty("--time", "initial");
circle.style.setProperty("--pauseHandler", "paused");
};
handleChange = ({ target }) => {
const { name } = target;
let { value } = target;
let bgMax;
let bgMin;
this.setState({
[name]: value
});
if (value === "0") {
if (name === "lBDelay") return;
this.addNotification("Session should last at least one minute", "danger");
setTimeout(() => this.setState({
[name]: 1
}), 800);
return;
}
if (name === "theme") {
// set values for background
bgMax = target.options[target.selectedIndex].dataset.max;
bgMin = target.options[target.selectedIndex].dataset.min;
updateTheme(value, bgMax, bgMin);
return;
}
};
// this method sets props in state so that the session is ready to start
// the session doesn't start automatically, but only when Start button is clicked!
handleSelect = selected => {
const {
sessionRunning,
timerId
} = this.state;
// check if session is already running. If yes display notification, if not update state so session is ready to start
if (sessionRunning === selected) {
this.addNotification("Session is already running", "warning");
return;
}
this.setState({
sessionReady: selected
}, () => this.prepareNewSession());
clearInterval(timerId);
// reset svg timer
let circle = document.querySelector("circle");
circle.style.setProperty("--time", "initial");
circle.style.setProperty("--pauseHandler", "paused");
};
validateForm = schema => {
const errors = this.validate(schema);
if (errors) {
const messages = Object.values(errors);
messages.map(m => this.addNotification(m, "danger"));
}
return errors;
};
validate = schema => {
const {
workTime,
shortBreakTime,
longBreakTime,
lBDelay
} = this.state;
const { error } = Joi.validate({
workTime: workTime,
shortBreakTime: shortBreakTime,
longBreakTime: longBreakTime,
lBDelay: lBDelay
},
schema, {
abortEarly: false
}
);
if (!error) return null;
let errors = {};
error.details.map(i => {
errors[i.path[0]] = i.message;
});
return errors;
};
saveChangesInSettings = () => {
this.prepareNewSession();
// trigger notification
this.addNotification("Changes have been saved!", "success");
};
progressTracker = () => {
const { pomodorosCompleted } = this.state;
return ( <
span className = "d-flex justify-content-center pb-5" > {
`You have completed ${pomodorosCompleted} ${
pomodorosCompleted === 1 ? "pomodoro" : "pomodoros"
}`
} <
/span>
);
};
alarm = new UIfx({
asset: alarm
});
render() {
const {
workTime,
shortBreakTime,
longBreakTime,
currentTime,
sessionReady,
lBDelay,
pomodorosCompleted,
theme,
sound
} = this.state;
return (
<React.Fragment>
<div id="background">
<Container>
<div>
<Navigationbar
activeKeyInNav={sessionReady}
workTime={workTime}
shortBreakTime={shortBreakTime}
longBreakTime={longBreakTime}
lBDelay={lBDelay}
theme={theme}
sound={sound}
saveChanges={this.saveChangesInSettings}
validateForm={this.validateForm}
onChange={this.handleChange}
onSelect={this.handleSelect}
/>
<ReactNotification ref={this.notificationDOMRef} />
<Timer
currentTime={currentTime}
pomodorosCompleted={pomodorosCompleted}
/>
<TimerControls
onStart={this.handleStart}
onStop={this.handleStop}
onReset={this.handleReset}
/>
</div>
<div id="backgroundLarge" />
</Container>
</div>
<Container className="pt-4">
<ToDoListForm />
<hr />
{this.progressTracker()}
</Container>
</React.Fragment>
);
}
}
export default App;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>