parent
85fd2f1ec8
commit
d6d9d780bb
@ -1,62 +0,0 @@
|
|||||||
# Calculator
|
|
||||||
|
|
||||||
**Tier:** 1-Beginner
|
|
||||||
|
|
||||||
Calculators are not only one of the most useful tools available, but they are
|
|
||||||
also a great way to understand UI and event processing in an application. In
|
|
||||||
this problem you will create a calculator that supports basic arithmetic
|
|
||||||
calculations on integers.
|
|
||||||
|
|
||||||
The styling is up to you so use your imagination and get creative! You might
|
|
||||||
also find it worth your time to experiment with the calculator app on your
|
|
||||||
mobile device to better understand basic functionality and edge cases.
|
|
||||||
|
|
||||||
### Constraints
|
|
||||||
|
|
||||||
- You may not use the `eval()` function to execute calculations
|
|
||||||
|
|
||||||
## User Stories
|
|
||||||
|
|
||||||
- [ ] User can see a display showing the current number entered or the
|
|
||||||
result of the last operation.
|
|
||||||
- [ ] User can see an entry pad containing buttons for the digits 0-9,
|
|
||||||
operations - '+', '-', '/', and '=', a 'C' button (for clear), and an 'AC'
|
|
||||||
button (for clear all).
|
|
||||||
- [ ] User can enter numbers as sequences up to 8 digits long by clicking on
|
|
||||||
digits in the entry pad. Entry of any digits more than 8 will be ignored.
|
|
||||||
- [ ] User can click on an operation button to display the result of that
|
|
||||||
operation on:
|
|
||||||
* the result of the preceding operation and the last number entered OR
|
|
||||||
* the last two numbers entered OR
|
|
||||||
* the last number entered
|
|
||||||
- [ ] User can click the 'C' button to clear the last number or the last
|
|
||||||
operation. If the users last entry was an operation the display will be
|
|
||||||
updated to the value that preceded it.
|
|
||||||
- [ ] User can click the 'AC' button to clear all internal work areas and
|
|
||||||
to set the display to 0.
|
|
||||||
- [ ] User can see 'ERR' displayed if any operation would exceed the
|
|
||||||
8 digit maximum.
|
|
||||||
|
|
||||||
## Bonus features
|
|
||||||
|
|
||||||
- [ ] User can click a '+/-' button to change the sign of the number that is
|
|
||||||
currently displayed.
|
|
||||||
- [ ] User can see a decimal point ('.') button on the entry pad to that
|
|
||||||
allows floating point numbers up to 3 places to be entered and operations to
|
|
||||||
be carried out to the maximum number of decimal places entered for any one
|
|
||||||
number.
|
|
||||||
|
|
||||||
## Useful links and resources
|
|
||||||
|
|
||||||
- [Calculator (Wikipedia)](https://en.wikipedia.org/wiki/Calculator)
|
|
||||||
- [MDN](https://developer.mozilla.org/en-US/)
|
|
||||||
|
|
||||||
## Example projects
|
|
||||||
|
|
||||||
- [BHMBS - JS-Neumorphic-Calculator](https://barhouum7.github.io/JS-Neumorphic-Calc.github.io/)
|
|
||||||
- [Javascript iOS Style Calculator](https://codepen.io/ssmkhrj/full/jOWBQqO)
|
|
||||||
- [Javascript Calculator](https://codepen.io/giana/pen/GJMBEv)
|
|
||||||
- [React Calculator](https://codepen.io/mjijackson/pen/xOzyGX)
|
|
||||||
- [Javascript-CALC](https://github.com/x0uter/javascript-calc)
|
|
||||||
- [Sample Calculator](https://sevlasnog.github.io/sample-calculator)
|
|
||||||
- [Python Calculator](https://github.com/kana800/Side-Projects/tree/master/1-Beginner/calculator)
|
|
@ -1,39 +1,16 @@
|
|||||||
.App {
|
.App {
|
||||||
text-align: center;
|
width:100vw;
|
||||||
|
max-width:100%;
|
||||||
|
min-height:100vh;
|
||||||
|
background: var(--primaryDark);
|
||||||
|
display: grid;
|
||||||
|
grid: 1fr 0.05fr / 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-logo {
|
.App > footer {
|
||||||
height: 40vmin;
|
width:100%;
|
||||||
pointer-events: none;
|
background:rgb(29, 29, 29);
|
||||||
}
|
text-align:center;
|
||||||
|
padding:10px;
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
color:white;
|
||||||
.App-logo {
|
|
||||||
animation: App-logo-float infinite 3s ease-in-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-header {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-link {
|
|
||||||
color: rgb(112, 76, 182);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes App-logo-float {
|
|
||||||
0% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { decrement, increment, selectCount } from './calculatorSlice';
|
||||||
|
import styles from './Calculator.module.css';
|
||||||
|
|
||||||
|
export function Calculator() {
|
||||||
|
const values = useSelector(selectCount);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<article className={styles.inputContainer}>
|
||||||
|
{values.map((value) => <span>{value}</span>)}
|
||||||
|
<input type='text' />
|
||||||
|
</article>
|
||||||
|
<article className={styles.buttonsContainer}>
|
||||||
|
<div className={styles.clearContainer}>
|
||||||
|
<button className={styles.clear}>C</button>
|
||||||
|
<button className={styles.clear}>AC</button>
|
||||||
|
</div>
|
||||||
|
<button className={styles.grey}>±</button>
|
||||||
|
<button className={styles.grey}>%</button>
|
||||||
|
<button className={styles.operator}>X</button>
|
||||||
|
<button>7</button>
|
||||||
|
<button>8</button>
|
||||||
|
<button>9</button>
|
||||||
|
<button className={styles.operator}>÷</button>
|
||||||
|
<button>4</button>
|
||||||
|
<button>5</button>
|
||||||
|
<button>6</button>
|
||||||
|
<button className={styles.operator}>-</button>
|
||||||
|
<button>1</button>
|
||||||
|
<button>2</button>
|
||||||
|
<button>3</button>
|
||||||
|
<button className={styles.operator}>+</button>
|
||||||
|
<button>0</button>
|
||||||
|
<button className={styles.grey}>.</button>
|
||||||
|
<button className={styles.result}>=</button>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
.section {
|
||||||
|
display: grid;
|
||||||
|
grid: 0.25fr 1fr / 1fr;
|
||||||
|
padding:25px;
|
||||||
|
background: var(--secondaryLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
display: grid;
|
||||||
|
place-items:center;
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer > input {
|
||||||
|
width:80%;
|
||||||
|
height:50%;
|
||||||
|
border-radius:5px;
|
||||||
|
border:0;
|
||||||
|
padding:5px;
|
||||||
|
box-shadow:2px 2px 2px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonsContainer {
|
||||||
|
display: grid;
|
||||||
|
grid: repeat(5, 1fr) / repeat(4, 1fr);
|
||||||
|
gap:5px;
|
||||||
|
border: 0.5px solid var(--secondaryDark);
|
||||||
|
padding:25px;
|
||||||
|
background:rgb(219, 219, 219);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonsContainer button {
|
||||||
|
border:0;
|
||||||
|
border-radius:1.5px;
|
||||||
|
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15);
|
||||||
|
cursor:pointer;
|
||||||
|
transition: filter 1s ease, background 1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonsContainer button:hover {
|
||||||
|
filter: brightness(125%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearContainer {
|
||||||
|
display: grid;
|
||||||
|
grid: 1fr / 1fr 1fr;
|
||||||
|
gap:1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
background: orangered;
|
||||||
|
color:white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear:hover {
|
||||||
|
background: rgb(255, 90, 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operator {
|
||||||
|
background: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
grid-column: span 2;
|
||||||
|
background: rgb(39, 218, 23);
|
||||||
|
color:white
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
values: [0],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const counterSlice = createSlice({
|
||||||
|
name: 'calculator',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
increment: (state) => {
|
||||||
|
state.value += 1;
|
||||||
|
},
|
||||||
|
decrement: (state) => {
|
||||||
|
state.value -= 1;
|
||||||
|
},
|
||||||
|
incrementByAmount: (state, action) => {
|
||||||
|
state.value += action.payload;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
|
||||||
|
|
||||||
|
export const selectCount = (state) => state.calculator.values;
|
||||||
|
|
||||||
|
export default counterSlice.reducer;
|
@ -1,67 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
import {
|
|
||||||
decrement,
|
|
||||||
increment,
|
|
||||||
incrementByAmount,
|
|
||||||
incrementAsync,
|
|
||||||
incrementIfOdd,
|
|
||||||
selectCount,
|
|
||||||
} from './counterSlice';
|
|
||||||
import styles from './Counter.module.css';
|
|
||||||
|
|
||||||
export function Counter() {
|
|
||||||
const count = useSelector(selectCount);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [incrementAmount, setIncrementAmount] = useState('2');
|
|
||||||
|
|
||||||
const incrementValue = Number(incrementAmount) || 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.row}>
|
|
||||||
<button
|
|
||||||
className={styles.button}
|
|
||||||
aria-label="Decrement value"
|
|
||||||
onClick={() => dispatch(decrement())}
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</button>
|
|
||||||
<span className={styles.value}>{count}</span>
|
|
||||||
<button
|
|
||||||
className={styles.button}
|
|
||||||
aria-label="Increment value"
|
|
||||||
onClick={() => dispatch(increment())}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.row}>
|
|
||||||
<input
|
|
||||||
className={styles.textbox}
|
|
||||||
aria-label="Set increment amount"
|
|
||||||
value={incrementAmount}
|
|
||||||
onChange={(e) => setIncrementAmount(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => dispatch(incrementByAmount(incrementValue))}
|
|
||||||
>
|
|
||||||
Add Amount
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={styles.asyncButton}
|
|
||||||
onClick={() => dispatch(incrementAsync(incrementValue))}
|
|
||||||
>
|
|
||||||
Add Async
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => dispatch(incrementIfOdd(incrementValue))}
|
|
||||||
>
|
|
||||||
Add If Odd
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row > button {
|
|
||||||
margin-left: 4px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
.row:not(:last-child) {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
font-size: 78px;
|
|
||||||
padding-left: 16px;
|
|
||||||
padding-right: 16px;
|
|
||||||
margin-top: 2px;
|
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
appearance: none;
|
|
||||||
background: none;
|
|
||||||
font-size: 32px;
|
|
||||||
padding-left: 12px;
|
|
||||||
padding-right: 12px;
|
|
||||||
outline: none;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
color: rgb(112, 76, 182);
|
|
||||||
padding-bottom: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: rgba(112, 76, 182, 0.1);
|
|
||||||
border-radius: 2px;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textbox {
|
|
||||||
font-size: 32px;
|
|
||||||
padding: 2px;
|
|
||||||
width: 64px;
|
|
||||||
text-align: center;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover,
|
|
||||||
.button:focus {
|
|
||||||
border: 2px solid rgba(112, 76, 182, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:active {
|
|
||||||
background-color: rgba(112, 76, 182, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.asyncButton {
|
|
||||||
composes: button;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asyncButton:after {
|
|
||||||
content: '';
|
|
||||||
background-color: rgba(112, 76, 182, 0.15);
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
opacity: 0;
|
|
||||||
transition: width 1s linear, opacity 0.5s ease 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asyncButton:active:after {
|
|
||||||
width: 0%;
|
|
||||||
opacity: 1;
|
|
||||||
transition: 0s;
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
// A mock function to mimic making an async request for data
|
|
||||||
export function fetchCount(amount = 1) {
|
|
||||||
return new Promise((resolve) =>
|
|
||||||
setTimeout(() => resolve({ data: amount }), 500)
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
|
||||||
import { fetchCount } from './counterAPI';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
value: 0,
|
|
||||||
status: 'idle',
|
|
||||||
};
|
|
||||||
|
|
||||||
// The function below is called a thunk and allows us to perform async logic. It
|
|
||||||
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
|
|
||||||
// will call the thunk with the `dispatch` function as the first argument. Async
|
|
||||||
// code can then be executed and other actions can be dispatched. Thunks are
|
|
||||||
// typically used to make async requests.
|
|
||||||
export const incrementAsync = createAsyncThunk(
|
|
||||||
'counter/fetchCount',
|
|
||||||
async (amount) => {
|
|
||||||
const response = await fetchCount(amount);
|
|
||||||
// The value we return becomes the `fulfilled` action payload
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const counterSlice = createSlice({
|
|
||||||
name: 'counter',
|
|
||||||
initialState,
|
|
||||||
// The `reducers` field lets us define reducers and generate associated actions
|
|
||||||
reducers: {
|
|
||||||
increment: (state) => {
|
|
||||||
// Redux Toolkit allows us to write "mutating" logic in reducers. It
|
|
||||||
// doesn't actually mutate the state because it uses the Immer library,
|
|
||||||
// which detects changes to a "draft state" and produces a brand new
|
|
||||||
// immutable state based off those changes
|
|
||||||
state.value += 1;
|
|
||||||
},
|
|
||||||
decrement: (state) => {
|
|
||||||
state.value -= 1;
|
|
||||||
},
|
|
||||||
// Use the PayloadAction type to declare the contents of `action.payload`
|
|
||||||
incrementByAmount: (state, action) => {
|
|
||||||
state.value += action.payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// The `extraReducers` field lets the slice handle actions defined elsewhere,
|
|
||||||
// including actions generated by createAsyncThunk or in other slices.
|
|
||||||
extraReducers: (builder) => {
|
|
||||||
builder
|
|
||||||
.addCase(incrementAsync.pending, (state) => {
|
|
||||||
state.status = 'loading';
|
|
||||||
})
|
|
||||||
.addCase(incrementAsync.fulfilled, (state, action) => {
|
|
||||||
state.status = 'idle';
|
|
||||||
state.value += action.payload;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
|
|
||||||
|
|
||||||
// The function below is called a selector and allows us to select a value from
|
|
||||||
// the state. Selectors can also be defined inline where they're used instead of
|
|
||||||
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
|
|
||||||
export const selectCount = (state) => state.counter.value;
|
|
||||||
|
|
||||||
// We can also write thunks by hand, which may contain both sync and async logic.
|
|
||||||
// Here's an example of conditionally dispatching actions based on current state.
|
|
||||||
export const incrementIfOdd = (amount) => (dispatch, getState) => {
|
|
||||||
const currentValue = selectCount(getState());
|
|
||||||
if (currentValue % 2 === 1) {
|
|
||||||
dispatch(incrementByAmount(amount));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default counterSlice.reducer;
|
|
@ -1,13 +1,13 @@
|
|||||||
body {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
padding: 0;
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
outline: 0;
|
||||||
sans-serif;
|
box-sizing: border-box;
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
:root {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
--primaryDark: #264653;
|
||||||
monospace;
|
--primaryLight: #2a9d8f;
|
||||||
|
--secondaryDark: #e76f51;
|
||||||
|
--secondaryLight: #f4a261;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import counterReducer from '../features/counter/counterSlice';
|
import calculatorReducer from '../features/calculator/calculatorSlice';
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
counter: counterReducer,
|
calculator: calculatorReducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in new issue