Skip to content

Commit

Permalink
React: Advanced counter
Browse files Browse the repository at this point in the history
  • Loading branch information
sadanandpai committed Jul 20, 2024
1 parent 9481cfb commit e786e59
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.main {
text-align: center;
font-size: 1.5rem;

section {
margin: 1rem 0;

div {
margin: 1rem 0;
}

button {
padding: 0.25rem 1rem;
margin: 0 0.5rem;
font-size: 1.5rem;
}
}

input {
width: 5rem;
padding: 0.15rem;
margin-left: 1rem;
font-size: 1.5rem;
}
}
65 changes: 65 additions & 0 deletions apps/react/src/challenges/advanced-counter/advanced-counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { MutableRefObject, useRef, useState } from 'react';
import { AsyncControls } from './components/async-controls';
import { SyncControls } from './components/sync-controls';
import { StepControl } from './components/step-control';
import { LimitControls } from './components/limit-controls';
import { DelayControl } from './components/delay-control';

import styles from './advanced-counter.module.scss';
import { maxLimit, minLimit } from './constants';

function AdvancedCounter() {
const [value, setValue] = useState(0);
const [step, setStep] = useState(1);
const [delay, setDelay] = useState(1);
const [lowerLimit, setLowerLimit] = useState(minLimit);
const [upperLimit, setUpperLimit] = useState(maxLimit);
const ref = useRef<{ reset: () => void }>({ reset: () => {} });

function reset() {
ref.current.reset();
setValue(0);
}

function stepBy(stepValue: number) {
setValue((prev) => {
const newValue = prev + stepValue;
if (lowerLimit <= newValue && newValue <= upperLimit) {
return newValue;
}

return prev;
});
}

return (
<main className={styles.main}>
<h2>{value}</h2>

<SyncControls stepBy={stepBy} step={step} />
<AsyncControls
delay={delay}
stepBy={stepBy}
step={step}
ref={ref as MutableRefObject<{ reset: () => void }>}
/>
<DelayControl delay={delay} setDelay={setDelay} />

<section>
<StepControl step={step} setStep={setStep} />

<LimitControls
value={value}
lowerLimit={lowerLimit}
upperLimit={upperLimit}
setLowerLimit={setLowerLimit}
setUpperLimit={setUpperLimit}
/>

<button onClick={reset}>Reset</button>
</section>
</main>
);
}

export default AdvancedCounter;
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { forwardRef, MutableRefObject, useImperativeHandle, useState } from 'react';
import styles from '../advanced-counter.module.scss';

interface Props {
delay: number;
step: number;
stepBy: (value: number) => void;
}

export const AsyncControls = forwardRef(function AsyncControls(
{ delay, step, stepBy }: Props,
ref: MutableRefObject<{ reset: () => void }>
) {
const [timerIds, setTimerIds] = useState<{
decrement: NodeJS.Timeout | null;
increment: NodeJS.Timeout | null;
}>({
decrement: null,
increment: null,
});

function decrementAsync() {
const stepValue = step;
const timerId = setTimeout(() => {
stepBy(-stepValue);
setTimerIds((state) => ({ ...state, decrement: null }));
}, delay * 1000);

setTimerIds((state) => ({ ...state, decrement: timerId }));
}

function incrementAsync() {
const stepValue = step;
const timerId = setTimeout(() => {
stepBy(+stepValue);
setTimerIds((state) => ({ ...state, increment: null }));
}, delay * 1000);

setTimerIds((state) => ({ ...state, increment: timerId }));
}

useImperativeHandle(ref, () => ({
reset: () => {
timerIds.decrement && clearTimeout(timerIds.decrement);
timerIds.increment && clearTimeout(timerIds.increment);
setTimerIds({
decrement: null,
increment: null,
});
},
}));

return (
<section className={styles.async}>
<button
onClick={decrementAsync}
aria-label="Async Decrement"
disabled={!!timerIds.decrement}
className={styles.asyncButton}
>
async -
</button>

<button
onClick={incrementAsync}
aria-label="Async Increment"
disabled={!!timerIds.increment}
className={styles.asyncButton}
>
+ async
</button>
</section>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { maxDelay, minDelay } from '../constants';

interface Props {
delay: number;
setDelay: React.Dispatch<React.SetStateAction<number>>;
}

export function DelayControl({ delay, setDelay }: Props) {
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const inputDelay = (e.target as HTMLInputElement).valueAsNumber;
setDelay(inputDelay);
}

return (
<div className="flex-center">
<label htmlFor="delay">Delay</label>

<input
type="range"
id="delay"
title="Delay value"
value={delay}
onChange={handleChange}
min={minDelay}
max={maxDelay}
step="1"
/>

<output htmlFor="delay">{delay}s</output>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { maxLimit, minLimit } from '../constants';

interface Props {
value: number;
lowerLimit: number;
upperLimit: number;
setLowerLimit: React.Dispatch<React.SetStateAction<number>>;
setUpperLimit: React.Dispatch<React.SetStateAction<number>>;
}

export function LimitControls({
value,
lowerLimit,
upperLimit,
setLowerLimit,
setUpperLimit,
}: Props) {
function lowerLimitHandler(e: React.ChangeEvent<HTMLInputElement>) {
const inputValue = e.target.valueAsNumber;

if (Number.isNaN(inputValue)) {
return setLowerLimit(minLimit);
} else if (inputValue > upperLimit) {
setLowerLimit(upperLimit);
} else if (inputValue < minLimit) {
setLowerLimit(minLimit);
} else if (inputValue > value) {
setLowerLimit(value);
} else {
setLowerLimit(inputValue);
}
}

function upperLimitHandler(e: React.ChangeEvent<HTMLInputElement>) {
const inputValue = e.target.valueAsNumber;

if (Number.isNaN(inputValue)) {
return setUpperLimit(maxLimit);
} else if (inputValue < lowerLimit) {
setUpperLimit(lowerLimit);
} else if (inputValue > maxLimit) {
setUpperLimit(maxLimit);
} else if (inputValue < value) {
setLowerLimit(value);
} else {
setUpperLimit(inputValue);
}
}

return (
<>
<div>
<label htmlFor="lowerLimit">Lower Limit</label>
<input
type="number"
id="lowerLimit"
title="Lower Limit"
value={lowerLimit}
onChange={lowerLimitHandler}
/>
</div>

<div>
<label htmlFor="upperLimit">Upper Limit</label>
<input
type="number"
id="upperLimit"
title="Upper Limit"
value={upperLimit}
onChange={upperLimitHandler}
/>
</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { maxStep, minStep } from '../constants';

interface Props {
step: number;
setStep: React.Dispatch<React.SetStateAction<number>>;
}

export const StepControl = function Step({ step, setStep }: Props) {
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.valueAsNumber;

if (Number.isNaN(value)) {
return setStep(1);
} else if (value > maxStep) {
setStep(maxStep);
} else if (value < minStep) {
setStep(minStep);
} else {
setStep(value);
}
}

return (
<div>
<label htmlFor="step">Increment/Decrement by</label>
<input
id="step"
type="number"
title="Step value"
min={minStep}
max={maxStep}
value={step}
onChange={handleChange}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import styles from '../advanced-counter.module.scss';

interface Props {
stepBy: (value: number) => void;
step: number;
}

export function SyncControls({ stepBy, step }: Props) {
return (
<section className={styles.async}>
<button onClick={() => stepBy(-step)} aria-label="Decrement">
-
</button>
<button onClick={() => stepBy(step)} aria-label="Increment">
+
</button>
</section>
);
}
6 changes: 6 additions & 0 deletions apps/react/src/challenges/advanced-counter/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const minStep = 1;
export const maxStep = 100;
export const minDelay = 1;
export const maxDelay = 3;
export const minLimit = -1000;
export const maxLimit = 1000;
4 changes: 2 additions & 2 deletions apps/react/src/challenges/counter/counter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRef, useState } from "react";
import { useRef, useState } from 'react';

import styles from "./counter.module.scss";
import styles from './counter.module.scss';

function Counter() {
const [value, setValue] = useState(0);
Expand Down
2 changes: 2 additions & 0 deletions apps/react/src/pages/Challenge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@ import Tab from '@/challenges/tab/App';
import DraggableList from '@/challenges/drag-drop/DraggableList';
import Circles from '@/challenges/circles/circles';
import AnalogClock from '@/challenges/analog-clock/analog-clock';
import AdvancedCounter from '@/challenges/advanced-counter/advanced-counter';

const reactChallengesMap = {
'transfer-list': <TransferListApp />,
counter: <Counter />,
'advanced-counter': <AdvancedCounter />,
accordion: <Accordion />,
'background-changer': <Background />,
'star-Rating': <StarRating />,
Expand Down
3 changes: 2 additions & 1 deletion apps/react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"paths": {
"@/*": ["./src/*"]
},
"types": ["vitest/globals"]
"types": ["vitest/globals"],
"strictFunctionTypes": false
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
Expand Down
10 changes: 10 additions & 0 deletions shared/data/content/react-challenges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,16 @@ const challenges = new Map<string, IChallenge>([
isNew: true,
},
],
[
'advanced-counter',
{
title: 'Advanced Counter',
link: 'advanced-counter',
difficulty: EDifficulty.Medium,
developer: 'sadanandpai',
tags: [ETag.interview],
},
],
]);

export const reactChallenges = sortChallengesByDifficulty(challenges);

0 comments on commit e786e59

Please sign in to comment.