Add task list demo application
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>Accident Counter</title>
|
<title>Accident Counter</title>
|
||||||
</head>
|
</head>
|
||||||
<script type="module" lang="jsx">
|
<script type="module">
|
||||||
import { createElement } from 'react';
|
import { createElement } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Counter } from './src/counter.tsx';
|
import { Counter } from './src/counter.tsx';
|
||||||
@@ -11,10 +11,7 @@
|
|||||||
|
|
||||||
createRoot(document.getElementById('root')).render(createElement(Counter));
|
createRoot(document.getElementById('root')).render(createElement(Counter));
|
||||||
</script>
|
</script>
|
||||||
<body>
|
<body class="container flex items-center justify-center min-h-screen">
|
||||||
<div
|
<div class="" id="root"></div>
|
||||||
class="container flex items-center justify-center h-screen"
|
|
||||||
id="root"
|
|
||||||
></div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vitest --ui",
|
"start": "vite dev",
|
||||||
"test": "vitest"
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -18,11 +19,12 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/stevekinney/testing-javascript#readme",
|
"homepage": "https://github.com/stevekinney/testing-javascript#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
|
"@testing-library/jest-dom": "^6.5.0",
|
||||||
"@testing-library/react": "^16.0.1",
|
"@testing-library/react": "^16.0.1",
|
||||||
"@types/react": "^18.3.6",
|
"@types/react": "^18.3.6",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/testing-library__jest-dom": "^5.14.9",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"@vitest/ui": "^2.1.1",
|
"@vitest/ui": "^2.1.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
'@tailwindcss/nesting': {},
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
85
examples/accident-counter/src/counter.test.jsx
Normal file
85
examples/accident-counter/src/counter.test.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { Counter } from './counter';
|
||||||
|
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
|
describe('Counter Component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<Counter />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with an initial count of 0', () => {
|
||||||
|
const countElement = screen.getByTestId('counter-count');
|
||||||
|
expect(countElement).toHaveTextContent('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays "days" when the count is 0', () => {
|
||||||
|
const unitElement = screen.getByTestId('counter-unit');
|
||||||
|
expect(unitElement).toHaveTextContent('days');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increments the count when the "Increment" button is clicked', async () => {
|
||||||
|
const incrementButton = screen.getByText('Increment');
|
||||||
|
await userEvent.click(incrementButton); // Using userEvent for a real click event
|
||||||
|
|
||||||
|
const countElement = screen.getByTestId('counter-count');
|
||||||
|
expect(countElement).toHaveTextContent('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays "day" when the count is 1', async () => {
|
||||||
|
const incrementButton = screen.getByText('Increment');
|
||||||
|
await userEvent.click(incrementButton); // Increment the count
|
||||||
|
|
||||||
|
const unitElement = screen.getByTestId('counter-unit');
|
||||||
|
expect(unitElement).toHaveTextContent('day');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decrements the count when the "Decrement" button is clicked', async () => {
|
||||||
|
const incrementButton = screen.getByText('Increment');
|
||||||
|
const decrementButton = screen.getByText('Decrement');
|
||||||
|
|
||||||
|
await userEvent.click(incrementButton); // Increment first
|
||||||
|
await userEvent.click(decrementButton); // Then decrement
|
||||||
|
|
||||||
|
const countElement = screen.getByTestId('counter-count');
|
||||||
|
expect(countElement).toHaveTextContent('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not allow decrementing below 0', async () => {
|
||||||
|
const decrementButton = screen.getByText('Decrement');
|
||||||
|
await userEvent.click(decrementButton); // Should not decrement below 0
|
||||||
|
|
||||||
|
const countElement = screen.getByTestId('counter-count');
|
||||||
|
expect(countElement).toHaveTextContent('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets the count when the "Reset" button is clicked', async () => {
|
||||||
|
const incrementButton = screen.getByText('Increment');
|
||||||
|
const resetButton = screen.getByText('Reset');
|
||||||
|
|
||||||
|
await userEvent.click(incrementButton); // Increment first
|
||||||
|
await userEvent.click(resetButton); // Then reset
|
||||||
|
|
||||||
|
const countElement = screen.getByTestId('counter-count');
|
||||||
|
expect(countElement).toHaveTextContent('0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables the "Decrement" and "Reset" buttons when the count is 0', () => {
|
||||||
|
const decrementButton = screen.getByText('Decrement');
|
||||||
|
const resetButton = screen.getByText('Reset');
|
||||||
|
|
||||||
|
expect(decrementButton).toBeDisabled();
|
||||||
|
expect(resetButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the document title based on the count', async () => {
|
||||||
|
const incrementButton = screen.getByText('Increment');
|
||||||
|
await userEvent.click(incrementButton);
|
||||||
|
|
||||||
|
expect(document.title).toBe('1 day');
|
||||||
|
|
||||||
|
await userEvent.click(incrementButton);
|
||||||
|
expect(document.title).toBe('2 days');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useReducer, useEffect } from 'react';
|
import { useReducer, useEffect } from 'react';
|
||||||
import { reducer } from './reducer';
|
import { reducer } from './reducer';
|
||||||
|
|
||||||
export const Counter = () => {
|
export const Counter = () => {
|
||||||
@@ -12,12 +12,12 @@ export const Counter = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="p-8 space-y-8 text-center bg-white border rounded-md shadow-lg border-slate-400">
|
<div className="p-8 space-y-8 text-center bg-white border rounded-md shadow-lg border-slate-400">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div data-test-id="counter-count" className="font-semibold text-8xl">
|
<div data-testid="counter-count" className="font-semibold text-8xl">
|
||||||
{state.count}
|
{state.count}
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<span data-test-id="counter-unit">{unit}</span> since the last
|
<span data-testid="counter-unit">{unit}</span> since the last
|
||||||
accident.
|
JavaScript-related accident.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { reducer } from './reducer';
|
import { reducer } from './reducer';
|
||||||
|
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
content: ['./**/*.{html,js,jsx,ts,tsx}'],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
container: {
|
|
||||||
center: true,
|
|
||||||
padding: '1rem',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
import { css } from 'css-configuration';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
css,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ import config from './vite.config';
|
|||||||
export default mergeConfig(config, {
|
export default mergeConfig(config, {
|
||||||
test: {
|
test: {
|
||||||
environment: 'happy-dom',
|
environment: 'happy-dom',
|
||||||
|
globals: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,393 +0,0 @@
|
|||||||
# Calculator Reducer
|
|
||||||
|
|
||||||
## Breakdown of the Code
|
|
||||||
|
|
||||||
## 1. **JSDoc Type Imports**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
/**
|
|
||||||
* @typedef {import('./types.js').CalculatorState} CalculatorState
|
|
||||||
* @typedef {import('./types.js').CalculatorAction} CalculatorAction
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
- These lines use **JSDoc** to import types from `types.js`.
|
|
||||||
- `CalculatorState` represents the shape of the calculator's state, and `CalculatorAction` represents the shape of an action dispatched to the reducer.
|
|
||||||
- These types provide type checking and documentation within the IDE, even though the code is in plain JavaScript.
|
|
||||||
|
|
||||||
## 2. **Initial State**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
/**
|
|
||||||
* The initial state for the calculator.
|
|
||||||
* @type {CalculatorState}
|
|
||||||
*/
|
|
||||||
const initialState = {
|
|
||||||
currentValue: '0', // Current input or result
|
|
||||||
previousValue: null, // Previous value before an operation
|
|
||||||
operator: null, // The operator (+, -, *, /)
|
|
||||||
waitingForOperand: false, // Tracks if we're waiting for the next operand
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
- `initialState` defines the starting values for the calculator.
|
|
||||||
- `currentValue`: Represents the value currently being input or the last calculated result. It starts as `'0'`.
|
|
||||||
- `previousValue`: Holds the value before an operator is pressed. Initially `null`.
|
|
||||||
- `operator`: Stores the operator (`+`, `-`, `*`, `/`) currently in use. It starts as `null`.
|
|
||||||
- `waitingForOperand`: A boolean flag that indicates if the calculator is waiting for the next operand after an operator is pressed.
|
|
||||||
|
|
||||||
## 3. **Reducer Function**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
/**
|
|
||||||
* A reducer function for the calculator state.
|
|
||||||
* @param {CalculatorState} state
|
|
||||||
* @param {CalculatorAction} action
|
|
||||||
* @returns {CalculatorState}
|
|
||||||
*/
|
|
||||||
export function calculatorReducer(state = initialState, action) {
|
|
||||||
```
|
|
||||||
|
|
||||||
- This is a **reducer function** that takes the current state and an action, and returns the new state based on the type of the action.
|
|
||||||
- **Default value**: If no state is provided (e.g., at initialization), it uses `initialState`.
|
|
||||||
|
|
||||||
## 4. **Handling Action Types**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
switch (action.type) {
|
|
||||||
case 'DIGIT':
|
|
||||||
```
|
|
||||||
|
|
||||||
- The `switch` statement handles different `action.type` values.
|
|
||||||
|
|
||||||
### 4.1 **Handling `'DIGIT'` Action**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
case 'DIGIT':
|
|
||||||
if (state.waitingForOperand) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentValue: action.payload,
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentValue:
|
|
||||||
state.currentValue === '0'
|
|
||||||
? action.payload
|
|
||||||
: state.currentValue + action.payload,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
- **When a digit is pressed** (`'DIGIT'` action):
|
|
||||||
- If the calculator is waiting for the next operand (`waitingForOperand` is `true`), it replaces the `currentValue` with the digit and resets the `waitingForOperand` flag.
|
|
||||||
- Otherwise, it appends the digit to the `currentValue`. If `currentValue` is `'0'`, it replaces it with the new digit.
|
|
||||||
|
|
||||||
### 4.2 **Handling `'OPERATOR'` Action**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
case 'OPERATOR':
|
|
||||||
if (state.operator && state.previousValue !== null) {
|
|
||||||
const result = evaluate(state);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
previousValue: result,
|
|
||||||
currentValue: '0',
|
|
||||||
operator: action.payload,
|
|
||||||
waitingForOperand: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
previousValue: state.currentValue,
|
|
||||||
operator: action.payload,
|
|
||||||
waitingForOperand: true,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
- **When an operator is pressed** (`'OPERATOR'` action):
|
|
||||||
- If there's already a `previousValue` and an operator is set, the function evaluates the current expression and stores the result as the `previousValue`.
|
|
||||||
- Otherwise, it sets the `currentValue` as the `previousValue` and assigns the operator from `action.payload`.
|
|
||||||
- It also sets `waitingForOperand` to `true` to signal that the calculator is waiting for the next number to be entered.
|
|
||||||
|
|
||||||
### 4.3 **Handling `'EQUALS'` Action**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
case 'EQUALS':
|
|
||||||
if (state.operator && state.previousValue !== null) {
|
|
||||||
const result = evaluate(state);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentValue: result,
|
|
||||||
previousValue: null,
|
|
||||||
operator: null,
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
```
|
|
||||||
|
|
||||||
- **When the equals button is pressed** (`'EQUALS'` action):
|
|
||||||
- If an operator and a `previousValue` are set, it evaluates the current expression and updates `currentValue` with the result. It also clears the operator and `previousValue`.
|
|
||||||
- If the necessary data for evaluation is not present, it simply returns the current state unchanged.
|
|
||||||
|
|
||||||
### 4.4 **Handling `'CLEAR'` Action**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
case 'CLEAR':
|
|
||||||
return initialState;
|
|
||||||
```
|
|
||||||
|
|
||||||
- **When the clear button is pressed** (`'CLEAR'` action):
|
|
||||||
- It resets the calculator state to the `initialState`.
|
|
||||||
|
|
||||||
### 4.5 **Default Case**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
```
|
|
||||||
|
|
||||||
- If an action type is not recognized, the reducer returns the current state without any changes.
|
|
||||||
|
|
||||||
## 5. **Evaluate Function**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function evaluate({ currentValue, previousValue, operator }) {
|
|
||||||
const prev = parseFloat(previousValue);
|
|
||||||
const current = parseFloat(currentValue);
|
|
||||||
|
|
||||||
switch (operator) {
|
|
||||||
case '+':
|
|
||||||
return (prev + current).toString();
|
|
||||||
case '-':
|
|
||||||
return (prev - current).toString();
|
|
||||||
case '*':
|
|
||||||
return (prev * current).toString();
|
|
||||||
case '/':
|
|
||||||
return current !== 0 ? (prev / current).toString() : 'Error';
|
|
||||||
default:
|
|
||||||
return currentValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- The `evaluate` function takes `currentValue`, `previousValue`, and `operator` to perform the calculation.
|
|
||||||
- **Steps**:
|
|
||||||
1. It converts the `previousValue` and `currentValue` from strings to numbers using `parseFloat`.
|
|
||||||
2. Based on the operator, it performs the corresponding arithmetic operation (`+`, `-`, `*`, `/`).
|
|
||||||
3. **Divide by zero handling**: If the operation is division (`/`) and `currentValue` is `0`, it returns `'Error'` to avoid division by zero.
|
|
||||||
4. The result of the operation is returned as a string.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- The `calculatorReducer` function handles the state changes of a calculator by interpreting different actions (`'DIGIT'`, `'OPERATOR'`, `'EQUALS'`, `'CLEAR'`).
|
|
||||||
- The `evaluate` helper function performs the arithmetic operations when required.
|
|
||||||
- The state is updated incrementally as the user presses digits, operators, and other controls, making it suitable for a basic calculator UI.
|
|
||||||
|
|
||||||
## Breakdown of the Tests
|
|
||||||
|
|
||||||
### 1. **Imports and Initial Setup**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { calculatorReducer } from './calculator.js'; // Assume this is the path to the reducer
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Vitest** is being imported for testing, using `describe` to group the test cases, `it` for individual test cases, and `expect` for assertions.
|
|
||||||
- The `calculatorReducer` is imported from a file called `calculator.js` for testing.
|
|
||||||
- The `CalculatorState` and `CalculatorAction` types are imported using **JSDoc** for static type checking.
|
|
||||||
|
|
||||||
### 2. **Initial State**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const initialState = {
|
|
||||||
currentValue: '0',
|
|
||||||
previousValue: null,
|
|
||||||
operator: null,
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
- This defines the `initialState`, representing the calculator's default state when no input has been provided yet.
|
|
||||||
|
|
||||||
### 3. **Test: Handling Digit Input**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
it('should handle a digit input', () => {
|
|
||||||
const action = { type: 'DIGIT', payload: '5' };
|
|
||||||
const newState = calculatorReducer(initialState, action);
|
|
||||||
expect(newState.currentValue).toBe('5');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
- **What it does**: Tests that when the `'DIGIT'` action is dispatched with the digit `'5'`, the `currentValue` is updated to `'5'`.
|
|
||||||
- **Why**: To ensure the reducer correctly updates the state with the inputted digit.
|
|
||||||
|
|
||||||
### 4. **Test: Appending Digits to Current Input**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
it('should append digits to the current input', () => {
|
|
||||||
const firstAction = { type: 'DIGIT', payload: '5' };
|
|
||||||
const secondAction = { type: 'DIGIT', payload: '3' };
|
|
||||||
|
|
||||||
let state = calculatorReducer(initialState, firstAction);
|
|
||||||
state = calculatorReducer(state, secondAction);
|
|
||||||
|
|
||||||
expect(state.currentValue).toBe('53');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
- **What it does**: Tests appending digits `'5'` and `'3'` to form `'53'`.
|
|
||||||
- **Why**: To verify that digits are appended to `currentValue` correctly and are not overwritten when a new digit is added.
|
|
||||||
|
|
||||||
### 5. **Test: Handling Operator Input**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
it('should handle operator input', () => {
|
|
||||||
const stateWithDigit = { ...initialState, currentValue: '10' };
|
|
||||||
const action = { type: 'OPERATOR', payload: '+' };
|
|
||||||
|
|
||||||
const newState = calculatorReducer(stateWithDigit, action);
|
|
||||||
|
|
||||||
expect(newState.previousValue).toBe('10');
|
|
||||||
expect(newState.operator).toBe('+');
|
|
||||||
expect(newState.waitingForOperand).toBe(true);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
- **What it does**: Tests pressing an operator (`'+'`), ensuring the state updates with the current value as `previousValue`, sets the operator, and sets `waitingForOperand` to `true`.
|
|
||||||
- **Why**: To confirm that when an operator is pressed, the reducer correctly prepares for the next operand.
|
|
||||||
|
|
||||||
### 6. **Test: Performing Addition**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
it('should perform addition when equals is pressed', () => {
|
|
||||||
const stateWithOperator = {
|
|
||||||
currentValue: '5',
|
|
||||||
previousValue: '10',
|
|
||||||
operator: '+',
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const action = { type: 'EQUALS' };
|
|
||||||
const newState = calculatorReducer(stateWithOperator, action);
|
|
||||||
|
|
||||||
expect(newState.currentValue).toBe('15');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
- **What it does**: Tests the `'EQUALS'` action when the operator is `'+'`, and the values are `10` and `5`. The result should be `'15'`.
|
|
||||||
- **Why**: To verify that the addition is correctly performed when the equals button is pressed.
|
|
||||||
|
|
||||||
### 7. **Test: Performing Subtraction**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
it('should perform subtraction when equals is pressed', () => {
|
|
||||||
const stateWithOperator = {
|
|
||||||
currentValue: '3',
|
|
||||||
previousValue: '8',
|
|
||||||
operator: '-',
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const action = { type: 'EQUALS' };
|
|
||||||
const newState = calculatorReducer(stateWithOperator, action);
|
|
||||||
|
|
||||||
expect(newState.currentValue).toBe('5');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
- **What it does**: Tests subtraction (`'8 - 3'`) when the equals button is pressed, with the result being `'5'`.
|
|
||||||
- **Why**: To confirm that subtraction works correctly with the equals action.
|
|
||||||
|
|
||||||
### 8. **Test: Performing Multiplication**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
it('should perform multiplication when equals is pressed', () => {
|
|
||||||
const stateWithOperator = {
|
|
||||||
currentValue: '4',
|
|
||||||
previousValue: '6',
|
|
||||||
operator: '*',
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const action = { type: 'EQUALS' };
|
|
||||||
const newState = calculatorReducer(stateWithOperator, action);
|
|
||||||
|
|
||||||
expect(newState.currentValue).toBe('24');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
- **What it does**: Tests multiplication (`'6 * 4'`) when the equals button is pressed, expecting the result `'24'`.
|
|
||||||
- **Why**: To ensure the multiplication operation is working correctly.
|
|
||||||
|
|
||||||
### 9. **Test: Performing Division**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
it('should perform division when equals is pressed', () => {
|
|
||||||
const stateWithOperator = {
|
|
||||||
currentValue: '2',
|
|
||||||
previousValue: '10',
|
|
||||||
operator: '/',
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const action = { type: 'EQUALS' };
|
|
||||||
const newState = calculatorReducer(stateWithOperator, action);
|
|
||||||
|
|
||||||
expect(newState.currentValue).toBe('5');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
- **What it does**: Tests division (`'10 / 2'`) with the expected result of `'5'`.
|
|
||||||
- **Why**: To confirm that division is handled properly.
|
|
||||||
|
|
||||||
### 10. **Test: Division by Zero**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
it('should return "Error" when dividing by zero', () => {
|
|
||||||
const stateWithOperator = {
|
|
||||||
currentValue: '0',
|
|
||||||
previousValue: '10',
|
|
||||||
operator: '/',
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const action = { type: 'EQUALS' };
|
|
||||||
const newState = calculatorReducer(stateWithOperator, action);
|
|
||||||
|
|
||||||
expect(newState.currentValue).toBe('Error');
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
- **What it does**: Tests dividing by zero (`'10 / 0'`) and expects the result to be `'Error'`.
|
|
||||||
- **Why**: To ensure the reducer correctly handles the edge case of division by zero, avoiding invalid arithmetic results.
|
|
||||||
|
|
||||||
### 11. **Test: Clearing the Calculator State**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
it('should reset the state when "CLEAR" action is dispatched', () => {
|
|
||||||
const stateWithValue = {
|
|
||||||
currentValue: '25',
|
|
||||||
previousValue: '5',
|
|
||||||
operator: '+',
|
|
||||||
waitingForOperand: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const action = { type: 'CLEAR' };
|
|
||||||
const newState = calculatorReducer(stateWithValue, action);
|
|
||||||
|
|
||||||
expect(newState).toEqual(initialState);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
- **What it does**: Tests that the `'CLEAR'` action resets the state back to its initial values (`initialState`).
|
|
||||||
- **Why**: To verify that pressing the clear button resets the entire calculator back to its default state.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- Each test case ensures that the **calculatorReducer** handles individual actions correctly.
|
|
||||||
- The tests cover all possible scenarios, including handling digits, appending digits, performing operations, evaluating expressions, handling edge cases like division by zero, and resetting the state.
|
|
||||||
- This ensures that the calculator works as expected in all cases and prevents regressions when changes are made to the code.
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {import('./types.js').CalculatorState} CalculatorState
|
|
||||||
* @typedef {import('./types.js').CalculatorAction} CalculatorAction
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The initial state for the calculator.
|
|
||||||
* @type {CalculatorState}
|
|
||||||
*/
|
|
||||||
const initialState = {
|
|
||||||
currentValue: '0', // Current input or result
|
|
||||||
previousValue: null, // Previous value before an operation
|
|
||||||
operator: null, // The operator (+, -, *, /)
|
|
||||||
waitingForOperand: false, // Tracks if we're waiting for the next operand
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A reducer function for the calculator state.
|
|
||||||
* @param {CalculatorState} state
|
|
||||||
* @param {CalculatorAction} action
|
|
||||||
* @returns {CalculatorState}
|
|
||||||
*/
|
|
||||||
export function calculatorReducer(state = initialState, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'DIGIT':
|
|
||||||
if (state.waitingForOperand) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentValue: action.payload, // Start a new value for next operand
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Append the digit
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentValue:
|
|
||||||
state.currentValue === '0'
|
|
||||||
? action.payload
|
|
||||||
: state.currentValue + action.payload,
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'OPERATOR':
|
|
||||||
// When an operator is pressed, store the current value and operator
|
|
||||||
if (state.operator && state.previousValue !== null) {
|
|
||||||
// If there's an operator already, evaluate the expression
|
|
||||||
const result = evaluate(state);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
previousValue: result,
|
|
||||||
currentValue: '0',
|
|
||||||
operator: action.payload,
|
|
||||||
waitingForOperand: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
previousValue: state.currentValue,
|
|
||||||
operator: action.payload,
|
|
||||||
waitingForOperand: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'EQUALS':
|
|
||||||
// Perform the operation when '=' is pressed
|
|
||||||
if (state.operator && state.previousValue !== null) {
|
|
||||||
const result = evaluate(state);
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
currentValue: result,
|
|
||||||
previousValue: null,
|
|
||||||
operator: null,
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
|
|
||||||
case 'CLEAR':
|
|
||||||
// Clear the calculator state
|
|
||||||
return initialState;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function evaluate({ currentValue, previousValue, operator }) {
|
|
||||||
const prev = parseFloat(previousValue);
|
|
||||||
const current = parseFloat(currentValue);
|
|
||||||
|
|
||||||
switch (operator) {
|
|
||||||
case '+':
|
|
||||||
return (prev + current).toString();
|
|
||||||
case '-':
|
|
||||||
return (prev - current).toString();
|
|
||||||
case '*':
|
|
||||||
return (prev * current).toString();
|
|
||||||
case '/':
|
|
||||||
return current !== 0 ? (prev / current).toString() : 'Error';
|
|
||||||
default:
|
|
||||||
return currentValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { calculatorReducer } from './calculator.js'; // Assume this is the path to the reducer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('./types.js').CalculatorState} CalculatorState
|
|
||||||
* @typedef {import('./types.js').CalculatorAction} CalculatorAction
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @type {import('./types.js').CalculatorState} */
|
|
||||||
const initialState = {
|
|
||||||
currentValue: '0',
|
|
||||||
previousValue: null,
|
|
||||||
operator: null,
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('calculatorReducer', () => {
|
|
||||||
it('should handle a digit input', () => {
|
|
||||||
/** @type {CalculatorAction} */
|
|
||||||
const action = { type: 'DIGIT', payload: '5' };
|
|
||||||
|
|
||||||
const newState = calculatorReducer(initialState, action);
|
|
||||||
|
|
||||||
expect(newState.currentValue).toBe('5');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should append digits to the current input', () => {
|
|
||||||
/** @type {CalculatorAction} */
|
|
||||||
const firstAction = { type: 'DIGIT', payload: '5' };
|
|
||||||
/** @type {CalculatorAction} */
|
|
||||||
const secondAction = { type: 'DIGIT', payload: '3' };
|
|
||||||
|
|
||||||
let state = calculatorReducer(initialState, firstAction);
|
|
||||||
state = calculatorReducer(state, secondAction);
|
|
||||||
|
|
||||||
expect(state.currentValue).toBe('53');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle operator input', () => {
|
|
||||||
const stateWithDigit = { ...initialState, currentValue: '10' };
|
|
||||||
/** @type {CalculatorAction} */
|
|
||||||
const action = { type: 'OPERATOR', payload: '+' };
|
|
||||||
|
|
||||||
const newState = calculatorReducer(stateWithDigit, action);
|
|
||||||
|
|
||||||
expect(newState.previousValue).toBe('10');
|
|
||||||
expect(newState.operator).toBe('+');
|
|
||||||
expect(newState.waitingForOperand).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should perform addition when equals is pressed', () => {
|
|
||||||
/** @type {CalculatorState} */
|
|
||||||
const stateWithOperator = {
|
|
||||||
currentValue: '5',
|
|
||||||
previousValue: '10',
|
|
||||||
operator: '+',
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {CalculatorAction} */
|
|
||||||
const action = { type: 'EQUALS' };
|
|
||||||
const newState = calculatorReducer(stateWithOperator, action);
|
|
||||||
|
|
||||||
expect(newState.currentValue).toBe('15');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should perform subtraction when equals is pressed', () => {
|
|
||||||
/** @type {CalculatorState} */
|
|
||||||
const stateWithOperator = {
|
|
||||||
currentValue: '3',
|
|
||||||
previousValue: '8',
|
|
||||||
operator: '-',
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {CalculatorAction} */
|
|
||||||
const action = { type: 'EQUALS' };
|
|
||||||
const newState = calculatorReducer(stateWithOperator, action);
|
|
||||||
|
|
||||||
expect(newState.currentValue).toBe('5');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should perform multiplication when equals is pressed', () => {
|
|
||||||
/** @type {CalculatorState} */
|
|
||||||
const stateWithOperator = {
|
|
||||||
currentValue: '4',
|
|
||||||
previousValue: '6',
|
|
||||||
operator: '*',
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {CalculatorAction} */
|
|
||||||
const action = { type: 'EQUALS' };
|
|
||||||
const newState = calculatorReducer(stateWithOperator, action);
|
|
||||||
|
|
||||||
expect(newState.currentValue).toBe('24');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should perform division when equals is pressed', () => {
|
|
||||||
/** @type {CalculatorState} */
|
|
||||||
const stateWithOperator = {
|
|
||||||
currentValue: '2',
|
|
||||||
previousValue: '10',
|
|
||||||
operator: '/',
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {CalculatorAction} */
|
|
||||||
const action = { type: 'EQUALS' };
|
|
||||||
const newState = calculatorReducer(stateWithOperator, action);
|
|
||||||
|
|
||||||
expect(newState.currentValue).toBe('5');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return "Error" when dividing by zero', () => {
|
|
||||||
/** @type {CalculatorState} */
|
|
||||||
const stateWithOperator = {
|
|
||||||
currentValue: '0',
|
|
||||||
previousValue: '10',
|
|
||||||
operator: '/',
|
|
||||||
waitingForOperand: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {CalculatorAction} */
|
|
||||||
const action = { type: 'EQUALS' };
|
|
||||||
const newState = calculatorReducer(stateWithOperator, action);
|
|
||||||
|
|
||||||
expect(newState.currentValue).toBe('Error');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reset the state when "CLEAR" action is dispatched', () => {
|
|
||||||
/** @type {CalculatorState} */
|
|
||||||
const stateWithValue = {
|
|
||||||
currentValue: '25',
|
|
||||||
previousValue: '5',
|
|
||||||
operator: '+',
|
|
||||||
waitingForOperand: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @type {CalculatorAction} */
|
|
||||||
const action = { type: 'CLEAR' };
|
|
||||||
const newState = calculatorReducer(stateWithValue, action);
|
|
||||||
|
|
||||||
expect(newState).toEqual(initialState);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
export type CalculatorState = {
|
|
||||||
currentValue: string;
|
|
||||||
previousValue: string | null;
|
|
||||||
operator: Operator | null;
|
|
||||||
waitingForOperand: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CalculatorAction =
|
|
||||||
| DigitAction
|
|
||||||
| OperatorAction
|
|
||||||
| EqualsAction
|
|
||||||
| ClearAction;
|
|
||||||
|
|
||||||
export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
|
|
||||||
export type Operator = '+' | '-' | '*' | '/';
|
|
||||||
|
|
||||||
export type ActionType = 'DIGIT' | 'OPERATOR' | 'EQUALS' | 'CLEAR';
|
|
||||||
|
|
||||||
type DigitAction = {
|
|
||||||
type: 'DIGIT';
|
|
||||||
payload: Digit;
|
|
||||||
};
|
|
||||||
|
|
||||||
type OperatorAction = {
|
|
||||||
type: 'OPERATOR';
|
|
||||||
payload: Operator;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EqualsAction = {
|
|
||||||
type: 'EQUALS';
|
|
||||||
payload?: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ClearAction = {
|
|
||||||
type: 'CLEAR';
|
|
||||||
payload?: never;
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
test: {
|
|
||||||
environment: 'happy-dom', // Use jsdom environment for browser testing
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
'@tailwindcss/nesting': {},
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1 +1 @@
|
|||||||
import '@testing-library/jest-dom'; // Provides custom matchers
|
import '@testing-library/jest-dom';
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
content: ['./**/*.{html,js}'],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
container: {
|
|
||||||
center: true,
|
|
||||||
padding: '1rem',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
import { css } from 'css-configuration';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
assetsInclude: ['**/*.html'],
|
assetsInclude: ['**/*.html'],
|
||||||
|
css,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
import { mergeConfig } from 'vitest/config';
|
||||||
|
import config from './vite.config';
|
||||||
|
|
||||||
export default defineConfig({
|
export default mergeConfig(config, {
|
||||||
assetsInclude: ['**/*.html'],
|
|
||||||
test: {
|
test: {
|
||||||
environment: 'happy-dom',
|
environment: 'happy-dom',
|
||||||
},
|
},
|
||||||
|
|||||||
13
examples/task-list/index.html
Normal file
13
examples/task-list/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Task List</title>
|
||||||
|
<script src="./src/index.tsx" type="module"></script>
|
||||||
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start": "concurrently \"npm run start:client\" \"npm run start:server\" --names \"client,server\" --kill-others -c blue,green",
|
||||||
|
"start:client": "vite dev",
|
||||||
|
"start:server": "node server/index.js",
|
||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -18,8 +21,20 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/stevekinney/testing-javascript#readme",
|
"homepage": "https://github.com/stevekinney/testing-javascript#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/body-parser": "^1.19.5",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"@vitest/ui": "^2.1.1",
|
"@vitest/ui": "^2.1.1",
|
||||||
"vite": "^5.4.5",
|
"chalk": "^5.3.0",
|
||||||
|
"concurrently": "^9.0.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"vite": "^5.4.6",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^2.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.3",
|
||||||
|
"express": "^4.21.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
examples/task-list/server/index.js
Normal file
65
examples/task-list/server/index.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTasks,
|
||||||
|
getTask,
|
||||||
|
createTask,
|
||||||
|
updateTask,
|
||||||
|
deleteTask,
|
||||||
|
} from './tasks.js';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
app.get('/api/tasks', (req, res) => {
|
||||||
|
const tasks = getTasks();
|
||||||
|
res.json(tasks);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/tasks', (req, res) => {
|
||||||
|
const { title } = req.body;
|
||||||
|
if (!title) {
|
||||||
|
return res.status(400).json({ message: 'A title is required' });
|
||||||
|
}
|
||||||
|
const task = createTask(title);
|
||||||
|
res.status(201).json(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/tasks/:id', (req, res) => {
|
||||||
|
const task = getTask(req.params.id);
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return res.status(404).json({ message: 'Task not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch('/api/tasks/:id', (req, res) => {
|
||||||
|
const { title, completed } = req.body;
|
||||||
|
|
||||||
|
const task = updateTask(req.params.id, { title, completed });
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return res.status(404).json({ message: 'Task not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/tasks/:id', (req, res) => {
|
||||||
|
const task = deleteTask(req.params.id);
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return res.status(404).json({ message: 'Task not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(204).send(); // No content to send back
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(chalk.magenta(`Server is running on port ${chalk.green(PORT)}…`));
|
||||||
|
});
|
||||||
65
examples/task-list/server/tasks.js
Normal file
65
examples/task-list/server/tasks.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { v4 as id } from 'uuid';
|
||||||
|
|
||||||
|
/** @typedef {import('../types').Task} Task */
|
||||||
|
|
||||||
|
/** @type {Task[]} tasks - An array to store tasks. */
|
||||||
|
let tasks = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all tasks.
|
||||||
|
* @returns {Task[]} An array of tasks.
|
||||||
|
*/
|
||||||
|
export const getTasks = () => tasks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new task.
|
||||||
|
* @param {string} title - The title of the task.
|
||||||
|
* @returns {Task} The newly created task.
|
||||||
|
*/
|
||||||
|
export const createTask = (title) => {
|
||||||
|
/** @type {Task} */
|
||||||
|
const task = {
|
||||||
|
id: id(),
|
||||||
|
title,
|
||||||
|
completed: false,
|
||||||
|
createdAt: new Date(),
|
||||||
|
lastModified: new Date(),
|
||||||
|
};
|
||||||
|
tasks.push(task);
|
||||||
|
return task;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a task by ID.
|
||||||
|
* @param {string} id - The ID of the task to find.
|
||||||
|
* @returns {Task | undefined} The found task or undefined if not found.
|
||||||
|
*/
|
||||||
|
export const getTask = (id) => tasks.find((task) => task.id === id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a task by ID.
|
||||||
|
* @param {string} id - The ID of the task to update.
|
||||||
|
* @param {Partial<Pick<Task, 'title' | 'description'>>} updates - The updates to apply to the task.
|
||||||
|
* @returns {Task | undefined} The updated task or undefined if not found.
|
||||||
|
*/
|
||||||
|
export const updateTask = (id, updates) => {
|
||||||
|
const task = getTask(id);
|
||||||
|
|
||||||
|
if (!task) return undefined;
|
||||||
|
|
||||||
|
Object.assign(task, updates, { lastModified: new Date() });
|
||||||
|
return task;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a task by ID.
|
||||||
|
* @param {string} id - The ID of the task to delete.
|
||||||
|
* @returns {boolean} `true` if the task was deleted, `false` if not found.
|
||||||
|
*/
|
||||||
|
export const deleteTask = (id) => {
|
||||||
|
const index = tasks.findIndex((task) => task.id === id);
|
||||||
|
if (index === -1) return false;
|
||||||
|
|
||||||
|
tasks.splice(index, 1);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
14
examples/task-list/src/application.tsx
Normal file
14
examples/task-list/src/application.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { CreateTask } from './create-task';
|
||||||
|
import { TaskProvider } from './task-context';
|
||||||
|
import { Tasks } from './tasks';
|
||||||
|
|
||||||
|
export const Application = () => {
|
||||||
|
return (
|
||||||
|
<TaskProvider>
|
||||||
|
<main className="container max-w-xl my-10 space-y-8">
|
||||||
|
<CreateTask onSubmit={(title) => console.log(title)} />
|
||||||
|
<Tasks />
|
||||||
|
</main>
|
||||||
|
</TaskProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
43
examples/task-list/src/create-task.tsx
Normal file
43
examples/task-list/src/create-task.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useTaskContext } from './task-context';
|
||||||
|
|
||||||
|
type CreateTaskProps = {
|
||||||
|
onSubmit: (title: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateTask = ({ onSubmit }: CreateTaskProps) => {
|
||||||
|
const { addTask } = useTaskContext();
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="/api/tasks"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
addTask(title);
|
||||||
|
setTitle('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="new-task-title" className="sr-only">
|
||||||
|
Title
|
||||||
|
</label>
|
||||||
|
<div className="flex">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
id="new-task-title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="What do you need to get done?"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={!title}>
|
||||||
|
Create Task
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
15
examples/task-list/src/date-time.tsx
Normal file
15
examples/task-list/src/date-time.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export const DateTime = ({ date, title }: { date: Date; title: string }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 text-xs sm:flex-row">
|
||||||
|
<h3 className="font-semibold sm:after:content-[':'] after:text-gray-900 text-primary-800">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
{date.toLocaleString(undefined, {
|
||||||
|
dateStyle: 'short',
|
||||||
|
timeStyle: 'short',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
9
examples/task-list/src/index.tsx
Normal file
9
examples/task-list/src/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { Application } from './application';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<Application />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
131
examples/task-list/src/task-context.tsx
Normal file
131
examples/task-list/src/task-context.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useReducer,
|
||||||
|
useEffect,
|
||||||
|
type ReactNode,
|
||||||
|
useContext,
|
||||||
|
} from 'react';
|
||||||
|
import { TasksActions, taskReducer, initialState } from './task-reducer';
|
||||||
|
import type { Task } from '../types';
|
||||||
|
|
||||||
|
interface TaskContextProps {
|
||||||
|
tasks: Task[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
addTask: (title: string) => void;
|
||||||
|
updateTask: (id: string, updatedTask: Partial<Task>) => void;
|
||||||
|
deleteTask: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskContext = createContext<TaskContextProps | undefined>(undefined);
|
||||||
|
|
||||||
|
interface TaskProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskProvider = ({ children }: TaskProviderProps) => {
|
||||||
|
const [state, dispatch] = useReducer(taskReducer, initialState);
|
||||||
|
|
||||||
|
// Fetch all tasks
|
||||||
|
const fetchTasks = async () => {
|
||||||
|
dispatch({ type: TasksActions.SET_LOADING });
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/tasks');
|
||||||
|
const data = await response.json();
|
||||||
|
dispatch({ type: TasksActions.FETCH_TASKS, payload: data });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: TasksActions.SET_ERROR,
|
||||||
|
payload: 'Failed to fetch tasks',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a new task
|
||||||
|
const addTask = async (title: string) => {
|
||||||
|
dispatch({ type: TasksActions.SET_LOADING });
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/tasks', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ title }),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
dispatch({ type: TasksActions.ADD_TASK, payload: data });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({ type: TasksActions.SET_ERROR, payload: 'Failed to add task' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update a task
|
||||||
|
const updateTask = async (id: string, updatedTask: Partial<Task>) => {
|
||||||
|
dispatch({ type: TasksActions.SET_LOADING });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/tasks/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(updatedTask),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update task');
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: TasksActions.UPDATE_TASK,
|
||||||
|
payload: { id, ...updatedTask },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: TasksActions.SET_ERROR,
|
||||||
|
payload: 'Failed to update task',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete a task
|
||||||
|
const deleteTask = async (id: string) => {
|
||||||
|
dispatch({ type: TasksActions.SET_LOADING });
|
||||||
|
try {
|
||||||
|
await fetch(`/api/tasks/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
dispatch({ type: TasksActions.DELETE_TASK, payload: id });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: TasksActions.SET_ERROR,
|
||||||
|
payload: 'Failed to delete task',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTasks();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TaskContext.Provider
|
||||||
|
value={{
|
||||||
|
tasks: state.tasks,
|
||||||
|
loading: state.loading,
|
||||||
|
error: state.error,
|
||||||
|
addTask,
|
||||||
|
updateTask,
|
||||||
|
deleteTask,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TaskContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useTaskContext = () => {
|
||||||
|
const context = useContext(TaskContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTaskContext must be used within a TaskProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TaskContext, TaskProvider, useTaskContext };
|
||||||
60
examples/task-list/src/task-reducer.ts
Normal file
60
examples/task-list/src/task-reducer.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { Task } from '../types';
|
||||||
|
|
||||||
|
export enum TasksActions {
|
||||||
|
FETCH_TASKS = 'fetch-tasks',
|
||||||
|
ADD_TASK = 'add-task',
|
||||||
|
UPDATE_TASK = 'update-task',
|
||||||
|
DELETE_TASK = 'delete-task',
|
||||||
|
SET_LOADING = 'set-loading',
|
||||||
|
SET_ERROR = 'set-error',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskState {
|
||||||
|
tasks: Task[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Action {
|
||||||
|
type: TasksActions;
|
||||||
|
payload?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initialState: TaskState = {
|
||||||
|
tasks: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const taskReducer = (state: TaskState, action: Action): TaskState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case TasksActions.FETCH_TASKS:
|
||||||
|
return { ...state, tasks: action.payload, loading: false };
|
||||||
|
case TasksActions.ADD_TASK:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tasks: [...state.tasks, action.payload],
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
case TasksActions.UPDATE_TASK:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tasks: state.tasks.map((task) =>
|
||||||
|
task.id === action.payload.id ? { ...task, ...action.payload } : task,
|
||||||
|
),
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
case TasksActions.DELETE_TASK:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tasks: state.tasks.filter((task) => task.id !== action.payload),
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
case TasksActions.SET_LOADING:
|
||||||
|
return { ...state, loading: true };
|
||||||
|
case TasksActions.SET_ERROR:
|
||||||
|
return { ...state, error: action.payload, loading: false };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
40
examples/task-list/src/task.tsx
Normal file
40
examples/task-list/src/task.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { DateTime } from './date-time';
|
||||||
|
import { useTaskContext } from './task-context';
|
||||||
|
|
||||||
|
type TaskProps = {
|
||||||
|
task: import('../types').Task;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Task = ({ task }: TaskProps) => {
|
||||||
|
const { updateTask, deleteTask } = useTaskContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="block p-4 space-y-2 border-t-2 border-x-2 last:border-b-2">
|
||||||
|
<header className="flex flex-row items-center gap-4">
|
||||||
|
<label htmlFor={`toggle-${task.id}`} className="sr-only">
|
||||||
|
Mark Task as {task.completed ? 'Incomplete' : 'Complete'}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={`toggle-${task.id}`}
|
||||||
|
type="checkbox"
|
||||||
|
className="block w-6 h-6"
|
||||||
|
checked={task.completed}
|
||||||
|
onChange={() => updateTask(task.id, { completed: !task.completed })}
|
||||||
|
/>
|
||||||
|
<h2 className="w-full font-semibold">{task.title}</h2>
|
||||||
|
<button
|
||||||
|
className="py-1 px-1.5 text-xs button-destructive button-ghost"
|
||||||
|
onClick={() => {
|
||||||
|
deleteTask(task.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div className="flex flex-col md:gap-2 md:flex-row">
|
||||||
|
<DateTime date={task.createdAt} title="Created" />
|
||||||
|
<DateTime date={task.lastModified} title="Modified" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
16
examples/task-list/src/tasks.tsx
Normal file
16
examples/task-list/src/tasks.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Task } from './task';
|
||||||
|
import { useTaskContext } from './task-context';
|
||||||
|
|
||||||
|
export const Tasks = () => {
|
||||||
|
const { tasks } = useTaskContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<ul>
|
||||||
|
{tasks.map((task) => (
|
||||||
|
<Task key={task.id} task={task} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
53
examples/task-list/styles.css
Normal file
53
examples/task-list/styles.css
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
label {
|
||||||
|
@apply block text-sm font-medium leading-6;
|
||||||
|
&[for]:has(~ input[required]) {
|
||||||
|
@apply after:content-['*'] after:text-red-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
input[type='email'],
|
||||||
|
input[type='password'] {
|
||||||
|
@apply text-sm sm:text-base block w-full rounded-md border-0 py-1.5 px-2.5 text-slate-900 shadow-sm ring-1 ring-inset ring-primary-700/20 placeholder:text-slate-400 sm:text-sm sm:leading-6 placeholder:text-primary-600;
|
||||||
|
@apply focus:ring-2 focus:ring-primary-600 focus:outline-none;
|
||||||
|
&:has(+ button[type='submit']) {
|
||||||
|
@apply block w-full rounded-r-none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
@apply h-4 w-4 rounded border-primary-500 accent-primary-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@apply button;
|
||||||
|
input + & {
|
||||||
|
@apply rounded-l-none -ml-px;
|
||||||
|
}
|
||||||
|
&[type='submit'] {
|
||||||
|
@apply button-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.button {
|
||||||
|
@apply relative inline-flex items-center justify-center gap-x-1.5 rounded-md px-3 py-2 text-sm font-semibold text-slate-900 ring-1 ring-inset ring-primary-300 hover:bg-slate-50 whitespace-pre disabled:cursor-not-allowed text-sm sm:text-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
@apply bg-primary-600 text-white cursor-pointer ring-primary-700 transition duration-100 ease-in-out hover:bg-primary-700 active:bg-primary-800 disabled:bg-primary-600/50 disabled:hover:bg-primary-600/50 disabled:active:bg-primary-600/50 disabled:cursor-not-allowed disabled:ring-primary-700/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-destructive {
|
||||||
|
@apply bg-red-600 text-white cursor-pointer ring-red-700 transition duration-100 ease-in-out hover:bg-red-700 active:bg-red-800 disabled:bg-red-600/50 disabled:hover:bg-red-600/50 disabled:active:bg-red-600/50 disabled:cursor-not-allowed disabled:ring-red-700/20;
|
||||||
|
&.button-ghost {
|
||||||
|
@apply bg-transparent text-red-600 ring-red-600 hover:bg-red-600/10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
examples/task-list/types.ts
Normal file
7
examples/task-list/types.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface Task {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
completed: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
lastModified: Date;
|
||||||
|
}
|
||||||
17
examples/task-list/vite.config.ts
Normal file
17
examples/task-list/vite.config.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { css } from 'css-configuration';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
css,
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
1826
package-lock.json
generated
1826
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,8 @@
|
|||||||
"examples/guess-the-number",
|
"examples/guess-the-number",
|
||||||
"examples/basic-math",
|
"examples/basic-math",
|
||||||
"examples/characters",
|
"examples/characters",
|
||||||
"examples/accident-counter"
|
"examples/accident-counter",
|
||||||
|
"packages/css-configuration"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.3.3"
|
"prettier": "^3.3.3"
|
||||||
|
|||||||
4
packages/css-configuration/index.d.ts
vendored
Normal file
4
packages/css-configuration/index.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* @type import('vite').UserConfig['css']
|
||||||
|
*/
|
||||||
|
export const css: import("vite").UserConfig["css"];
|
||||||
42
packages/css-configuration/index.js
Normal file
42
packages/css-configuration/index.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import nesting from 'tailwindcss/nesting/index.js';
|
||||||
|
import tailwindcss from 'tailwindcss';
|
||||||
|
import autoprefixer from 'autoprefixer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type import('vite').UserConfig['css']
|
||||||
|
*/
|
||||||
|
export const css = {
|
||||||
|
postcss: {
|
||||||
|
plugins: [
|
||||||
|
tailwindcss({
|
||||||
|
content: ['./**/*.{html,js,jsx,ts,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: '1rem',
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f3faeb',
|
||||||
|
100: '#e5f3d4',
|
||||||
|
200: '#cde8ae',
|
||||||
|
300: '#acd87e',
|
||||||
|
400: '#8ec655',
|
||||||
|
500: '#6ca635',
|
||||||
|
600: '#558828',
|
||||||
|
700: '#426823',
|
||||||
|
800: '#375420',
|
||||||
|
900: '#30481f',
|
||||||
|
950: '#17270c',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}),
|
||||||
|
nesting,
|
||||||
|
autoprefixer,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "calculator-reducer",
|
"name": "css-configuration",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Yet another attempt at implementing a calculator, but this time with a reducer.",
|
"description": "Some CSS settings for Vite/PostCSS that includes Tailwind",
|
||||||
"main": "src/index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vitest --ui",
|
"types": "tsc --emitDeclarationOnly --allowJs --declaration --skipLibCheck index.js"
|
||||||
"test": "vitest"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -19,7 +18,9 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/stevekinney/testing-javascript#readme",
|
"homepage": "https://github.com/stevekinney/testing-javascript#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/ui": "^2.1.1",
|
"autoprefixer": "^10.4.20",
|
||||||
"vitest": "^2.1.1"
|
"tailwindcss": "^3.4.12",
|
||||||
|
"typescript": "^5.6.2",
|
||||||
|
"vite": "^5.4.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"paths": {
|
||||||
|
"*": ["node_modules/*"]
|
||||||
|
},
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user