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 {
|
||||
text-align: center;
|
||||
width:100vw;
|
||||
max-width:100%;
|
||||
min-height:100vh;
|
||||
background: var(--primaryDark);
|
||||
display: grid;
|
||||
grid: 1fr 0.05fr / 1fr;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.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);
|
||||
}
|
||||
}
|
||||
.App > footer {
|
||||
width:100%;
|
||||
background:rgb(29, 29, 29);
|
||||
text-align:center;
|
||||
padding:10px;
|
||||
color:white;
|
||||
}
|
@ -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;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
padding: 0;
|
||||
outline: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
:root {
|
||||
--primaryDark: #264653;
|
||||
--primaryLight: #2a9d8f;
|
||||
--secondaryDark: #e76f51;
|
||||
--secondaryLight: #f4a261;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import counterReducer from '../features/counter/counterSlice';
|
||||
import calculatorReducer from '../features/calculator/calculatorSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
counter: counterReducer,
|
||||
calculator: calculatorReducer,
|
||||
},
|
||||
});
|
||||
|
Loading…
Reference in new issue