Skip to content

fluxthedev/chatgpt-4-nextjs-calc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Purpose

For researching how well ChatGPT-4 can setup a slightly complex app, provide successful solutions for any issues with the app, and able to provide updated code for new features

Prompt

You are a reactJS developer of 8 years. Create for me a Next.js app using the boilerplate: create-next-app in NPX. The app will be a simple addition, subtraction, division and multiplication calculator. You will have reusable components and subcomponents, and use tailwindCSA for the css. The visual part should look like an actual calculator.

Response

To create a Next.js calculator application, you would first need to setup your project with create-next-app and then install Tailwind CSS. Below are the steps to do that:

  1. Setting Up Project

Open your terminal and run:

npx create-next-app@latest calculator-app
cd calculator-app
  1. Setting Up Tailwind CSS

Run the following commands:

npm install tailwindcss postcss autoprefixer postcss-cli
npx tailwindcss init -p

You'll now have a tailwind.config.js and a postcss.config.js file in your root directory.

Replace the content of your postcss.config.js file with this:

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

In your tailwind.config.js file, add this to expose all the styles:

module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Next, create a new CSS file globals.css in the styles directory and add the following code:

@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

Finally, include the CSS file in _app.js file:

import '../styles/globals.css'

Now you're all set up with Tailwind CSS.

Next, we will create the components. For a calculator, we need a Calculator component with subcomponents Display and Button.

  1. Creating Components

Under components folder:

  • Create Calculator.js:
import { useState } from "react";
import Display from "./Display";
import Button from "./Button";

const Calculator = () => {
  const [display, setDisplay] = useState("");

  const handleClick = (value) => {
    if (value === "=") {
      try {
        setDisplay(eval(display));
      } catch {
        setDisplay("Error");
      }
    } else if (value === "C") {
      setDisplay("");
    } else {
      setDisplay(display + value);
    }
  };

  const buttons = ["7", "8", "9", "/", "4", "5", "6", "*", "1", "2", "3", "-", "0", ".", "=", "+", "C"];

  return (
    <div className="p-10 grid grid-cols-4 gap-4">
      <Display value={display} />
      {buttons.map((button, index) => (
        <Button key={index} value={button} onClick={() => handleClick(button)} />
      ))}
    </div>
  );
};

export default Calculator;
  • Create Display.js:
const Display = ({ value }) => (
  <div className="col-span-4 bg-gray-800 text-white text-right py-6 px-10 rounded">
    {value}
  </div>
);

export default Display;
  • Create Button.js:
const Button = ({ value, onClick }) => (
  <button
    className="bg-gray-800 text-white w-full py-3 rounded"
    onClick={onClick}
  >
    {value}
  </button>
);

export default Button;
  1. Updating the Main Page

Finally, replace the content of pages/index.js with this:

import Head from 'next/head'
import Calculator from '../components/

Calculator'

export default function Home() {
  return (
    <div className="flex items-center justify-center min-h-screen bg-gray-900">
      <Head>
        <title>Calculator App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <Calculator />
    </div>
  )
}
  1. Running Your Application

Run your application with:

npm run dev

This should give you a simple calculator application in Next.js with Tailwind CSS. It includes an addition, subtraction, multiplication and division operation. It's styled to look like an actual calculator. Buttons and Display are reusable components. The logic for the calculator is kept within the Calculator component.

Follow-up Prompt

What about the utility functions for addition, subtraction, multiplication, and divison?

Response

Indeed, the use of eval in JavaScript code is often discouraged due to security and debugging issues. To improve upon this, we could create a utility function for each operation.

Let's create a new file named utils.js in the utils folder:

export const add = (num1, num2) => Number(num1) + Number(num2);

export const subtract = (num1, num2) => Number(num1) - Number(num2);

export const multiply = (num1, num2) => Number(num1) * Number(num2);

export const divide = (num1, num2) => {
  if (Number(num2) === 0) {
    throw new Error('Cannot divide by zero');
  }

  return Number(num1) / Number(num2);
};

These are simple JavaScript functions that perform the desired operations. We convert the inputs to numbers to ensure that the operations work as expected (e.g., the addition operation doesn't just concatenate strings).

Now, to incorporate these functions in our Calculator component, we will modify the handleClick function a little bit. We will store the current operation and the first operand, then perform the calculation when the equals button is pressed:

import { useState } from "react";
import { add, subtract, multiply, divide } from '../utils/utils';
import Display from "./Display";
import Button from "./Button";

const Calculator = () => {
  const [display, setDisplay] = useState("");
  const [firstOperand, setFirstOperand] = useState(null);
  const [currentOperation, setCurrentOperation] = useState(null);

  const handleClick = (value) => {
    switch (value) {
      case "+":
      case "-":
      case "*":
      case "/":
        setFirstOperand(display);
        setCurrentOperation(value);
        setDisplay("");
        break;
      case "=":
        if (firstOperand !== null && currentOperation) {
          switch (currentOperation) {
            case "+":
              setDisplay(add(firstOperand, display));
              break;
            case "-":
              setDisplay(subtract(firstOperand, display));
              break;
            case "*":
              setDisplay(multiply(firstOperand, display));
              break;
            case "/":
              try {
                setDisplay(divide(firstOperand, display));
              } catch (e) {
                setDisplay(e.message);
              }
              break;
            default:
              break;
          }
          setFirstOperand(null);
          setCurrentOperation(null);
        }
        break;
      case "C":
        setDisplay("");
        setFirstOperand(null);
        setCurrentOperation(null);
        break;
      default:
        setDisplay(display + value);
        break;
    }
  };

  const buttons = ["7", "8", "9", "/", "4", "5", "6", "*", "1", "2", "3", "-", "0", ".", "=", "+", "C"];

  return (
    <div className="p-10 grid grid-cols-4 gap-4">
      <Display value={display} />
      {buttons.map((button, index) => (
        <Button key={index} value={button} onClick={() => handleClick(button)} />
      ))}
    </div>
  );
};

export default Calculator;

Now, the application uses the utility functions to perform calculations instead of eval. This approach is safer and easier to debug and maintain. It also handles the divide by zero case separately to avoid errors.

Minor Tweak

After I followed the guide above I noticed the following error when I ran the app:

1 | import { useState } from 'react'; : ^^^^^^^^
2 | import { add, subtract, multiply, divide } from '../utils/utils';
3 | import Display from './Display'; 
4 | import Button from './Button'; `---- Maybe one of these should be marked as a client entry with "use client": ./app/components/Calculator.js ./app/page.js

I manually fixed it by adding use client at at top of the files components/Calculator.js and page.js

I wasn't using the web-view of ChatGPT-4 for this but based on some information I found online, this seems to be a more recent "fix" for Next.js (after 2021).

Follow-up Prompt

Some feedback from running the app, that I need fixed: 1. Instead of clearing the input when an operator is pressed, keep the number and the operator in the display until = is pressed. When equal is pressed, show the final value. 2. The calculator display is too small, make it wider 3. The calculator buttons expand as the display number gets bigger

Response

  1. Update the handling of the operator buttons - Instead of clearing the display immediately when an operator is pressed, we can append the operator to the display and wait for the next number to be input:
  const handleClick = (value) => {
    switch (value) {
      case "+":
      case "-":
      case "*":
      case "/":
        if (firstOperand === null) {
          setFirstOperand(display);
        } else {
          performCalculation();
        }
        setCurrentOperation(value);
        setDisplay(display + value);
        break;
      // rest of the code remains the same
  };

This modification also requires a helper function to perform the calculation:

  const performCalculation = () => {
    switch (currentOperation) {
      case "+":
        setDisplay((prev) => prev.slice(0, -1) + add(firstOperand, display.slice(0, -1)));
        break;
      case "-":
        setDisplay((prev) => prev.slice(0, -1) + subtract(firstOperand, display.slice(0, -1)));
        break;
      case "*":
        setDisplay((prev) => prev.slice(0, -1) + multiply(firstOperand, display.slice(0, -1)));
        break;
      case "/":
        try {
          setDisplay((prev) => prev.slice(0, -1) + divide(firstOperand, display.slice(0, -1)));
        } catch (e) {
          setDisplay(e.message);
        }
        break;
      default:
        break;
    }
    setFirstOperand(null);
  };

We update the display with the result of the operation, then remove the current operation and first operand.

  1. Make the calculator display wider - In the Display.js component, add a w-full class:
  <div className="col-span-4 w-full bg-gray-800 text-white text-right py-6 px-10 rounded">
    {value}
  </div>
  1. Prevent buttons from expanding as the display number gets bigger - Buttons are currently set to occupy the full width of their parent. This behavior can be prevented by setting a fixed width:
  <button
    className="bg-gray-800 text-white w-20 py-3 rounded"
    onClick={onClick}
  >
    {value}
  </button>

If the buttons are still expanding, it could be due to the CSS grid expanding to accommodate the larger display number. To fix this, you could set a max width on the grid or on the calculator container:

  return (
    <div className="p-10 grid grid-cols-4 gap-4 max-w-lg">
      <Display value={display} />
      {buttons.map((button, index) => (
        <Button key={index} value={button} onClick={() => handleClick(button)} />
      ))}
    </div>
  );

With these modifications, the calculator should function and look more as you expect.

Follow-up Prompt

When running the app, pressing 5, then +, then 5, I get NaN when I press = with the following error in the console, please fix: Received NaN for the children attribute. If this is expected, cast the value to a string.

Response

The issue appears to be coming from a type mismatch between the functions in utils.js and the setDisplay method in Calculator.js. The functions in utils.js return a number, while setDisplay expects a string.

This can be resolved by ensuring that the output of the performCalculation function is converted to a string before it's passed to setDisplay. Here's how you can update the performCalculation function to fix this:

const performCalculation = () => {
    switch (currentOperation) {
        case "+":
            setDisplay((prev) => prev.slice(0, -1) + add(Number(firstOperand), Number(display.slice(0, -1))).toString());
            break;
        case "-":
            setDisplay((prev) => prev.slice(0, -1) + subtract(Number(firstOperand), Number(display.slice(0, -1))).toString());
            break;
        case "*":
            setDisplay((prev) => prev.slice(0, -1) + multiply(Number(firstOperand), Number(display.slice(0, -1))).toString());
            break;
        case "/":
            try {
                setDisplay((prev) => prev.slice(0, -1) + divide(Number(firstOperand), Number(display.slice(0, -1))).toString());
            } catch (e) {
                setDisplay(e.message);
            }
            break;
        default:
            break;
    }
    setFirstOperand(null);
};

This change will convert the output of the math functions to strings, which should remove the error you're seeing. Now when you press "=", you should see the correct result.

Please remember to apply this change to the "=" case inside the handleClick function as well.

Follow-up Prompt

I think I do need to apply the case to the handleClick function as I am still getting NaN when I click =. Please show the code for this

Response

I apologize for the confusion. If you are still experiencing issues, let's refactor the handleClick method to correctly manage the application's state.

This is how the modified handleClick method would look:

const handleClick = (value) => {
  switch (value) {
    case "+":
    case "-":
    case "*":
    case "/":
      if (firstOperand === null) {
        setFirstOperand(parseFloat(display));
      } else if (currentOperation) {
        setFirstOperand(performCalculation());
      }
      setCurrentOperation(value);
      setDisplay((prev) => prev + value);
      break;
    case "=":
      if (firstOperand !== null && currentOperation) {
        const result = performCalculation();
        setFirstOperand(null);
        setCurrentOperation(null);
        setDisplay(result.toString());
      }
      break;
    case "C":
      setDisplay("");
      setFirstOperand(null);
      setCurrentOperation(null);
      break;
    default:
      setDisplay((prev) => prev + value);
      break;
  }
};

The performCalculation method will return the result of the operation, and we will convert it to a string before setting it as the new display value. Here's the updated performCalculation method:

const performCalculation = () => {
  switch (currentOperation) {
    case "+":
      return add(parseFloat(firstOperand), parseFloat(display.slice(firstOperand.length + 1)));
    case "-":
      return subtract(parseFloat(firstOperand), parseFloat(display.slice(firstOperand.length + 1)));
    case "*":
      return multiply(parseFloat(firstOperand), parseFloat(display.slice(firstOperand.length + 1)));
    case "/":
      try {
        return divide(parseFloat(firstOperand), parseFloat(display.slice(firstOperand.length + 1)));
      } catch (e) {
        setDisplay(e.message);
      }
      break;
    default:
      break;
  }
};

This change should resolve the issue you're facing with getting NaN on pressing =.

Follow-up Prompt

That did resolve it thank you. One more issue, when I press any button, the display height get's wider. Please fix.

Response

The issue could be caused by the text content in the Display component wrapping to a new line when it exceeds the width of the component. This could be fixed by applying the CSS property overflow: auto and white-space: nowrap to prevent line breaks in the Display component. In Tailwind CSS, this can be achieved by adding the classes overflow-auto and whitespace-nowrap to the div element in the Display component.

Here is the updated Display.js component:

const Display = ({ value }) => {
  return (
    <div className="col-span-4 w-full bg-gray-800 text-white text-right py-6 px-10 rounded overflow-auto whitespace-nowrap">
      {value}
    </div>
  );
};

export default Display;

The overflow-auto class will add a scrollbar if the content overflows the div's width, while the whitespace-nowrap class will prevent the content from wrapping to the next line, effectively maintaining the height of the Display component.

Please note that with this setting, if the equation or result is too long, it will be cut-off from the view. You might want to consider limiting the length of the input or result to prevent this from happening.

Follow-up Prompt

That's not the solution. The height of the display gets bigger before the wrap

Response

I apologize for misunderstanding your issue. The change in display height might be caused by a default vertical alignment issue when adding content to the display.

You can prevent the change in height by applying a fixed height to the display. Here's how you can do it:

const Display = ({ value }) => {
  return (
    <div className="col-span-4 w-full h-20 bg-gray-800 text-white text-right py-6 px-10 rounded">
      {value}
    </div>
  );
};

export default Display;

In this snippet, the h-20 class sets a fixed height for the display. You can adjust the value 20 to meet your requirements.

Also, ensure that the py-6 padding value doesn't exceed the total height (i.e., h-20). If the padding is too large, it can cause the container's dimensions to expand beyond the set height.

This change will make the display height remain constant regardless of the content.

Follow-up Prompt

Make the Display component use FlexBox as well

Response

Sure, you can use Flexbox for the Display component as well. Using Flexbox can help center the text both horizontally and vertically.

Here is how you can modify your Display component:

const Display = ({ value }) => {
  return (
    <div className="flex justify-end items-center w-full h-20 bg-gray-800 text-white p-4 rounded">
      {value}
    </div>
  );
};

export default Display;

In this case, I've used flex to declare a flex container. The justify-end class is used to align the items (the calculator display text) along the end of the horizontal axis. The items-center class is used to center the items along the vertical axis.

The w-full h-20 classes are used to define the width (full width) and height of the component. The p-4 class is used to add padding around the component. The rounded class gives the component rounded corners.

You may need to adjust the padding and height according to your specific design requirements.

Follow-up Prompt

The Display component isn't above the buttons. It is in the left corner of the buttons and around the same size as the buttons. Please make it above the buttons and stretch full width.

Response

I apologize for misunderstanding your layout requirements. In your current setup, the Display and Button components are being treated as equal flex items within the Calculator component, which is causing them to be positioned side by side. If you want the Display to be positioned above the buttons and stretched to full width, you'll need to make a small adjustment.

One way to solve this is to use flex-col in your Calculator component, which will align the children (the Display and Button components) in a column instead of in a row.

Here's the adjusted code for the Calculator.js component:

import { useState } from 'react';
import { add, subtract, multiply, divide } from '../utils/utils';
import Display from './Display';
import Button from './Button';

const Calculator = () => {
  const [display, setDisplay] = useState("");
  const [firstOperand, setFirstOperand] = useState(null);
  const [currentOperation, setCurrentOperation] = useState(null);

  const buttons = [
    '7', '8', '9', '/',
    '4', '5', '6', '*',
    '1', '2', '3', '-',
    '0', '.', '=', '+',
    'C'
  ];

  const handleClick = /* Your handleClick function */

  const performCalculation = /* Your performCalculation function */

  return (
    <div className="p-10 flex flex-col items-center w-full max-w-lg mx-auto">
      <Display value={display} />
      <div className="flex flex-wrap justify-center">
        {buttons.map((button, index) => (
          <Button key={index} value={button} onClick={() => handleClick(button)} />
        ))}
      </div>
    </div>
  );
};

export default Calculator;

Now, the Display component will be on top, stretched to full width, and the buttons will be underneath. Also, the w-full class in the Display component will stretch the display to full width of the parent container.

const Display = ({ value }) => {
  return (
    <div className="flex justify-end items-center w-full h-20 bg-gray-800 text-white p-4 rounded">
      {value}
    </div>
  );
};

export default Display;

This adjustment should give you the layout you want.

Follow-up Prompt

Good, but 3 issues remain: 1. the display is wider than the buttons, 2. padding below the display needs to be the same as between the buttons 3. the C button needs to be left aligned

Response

I apologize for the oversight. Here's how we can fix those issues:

  1. To ensure that the Display component has the same width as the group of buttons, you can apply a max-width to the Calculator component using a custom class.

  2. To provide the same padding below the display as between the buttons, you can adjust the padding in the Calculator component and Button component accordingly.

  3. To left align the "C" button, you can add an extra conditional rendering for this button.

Here's how you can modify your components:

// app/components/Calculator.js
import { useState } from 'react';
import { add, subtract, multiply, divide } from '../utils/utils';
import Display from './Display';
import Button from './Button';

const Calculator = () => {
  const [display, setDisplay] = useState("");
  const [firstOperand, setFirstOperand] = useState(null);
  const [currentOperation, setCurrentOperation] = useState(null);

  const buttons = [
    '7', '8', '9', '/',
    '4', '5', '6', '*',
    '1', '2', '3', '-',
    '0', '.', '=', '+'
  ];

  const handleClick = /* Your handleClick function */

  const performCalculation = /* Your performCalculation function */

  return (
    <div className="p-10 flex flex-col items-center w-full max-w-lg mx-auto">
      <Display value={display} />
      <div className="flex flex-wrap justify-center">
        {buttons.map((button, index) => (
          <Button key={index} value={button} onClick={() => handleClick(button)} />
        ))}
        <div className="w-20">
          <Button value="C" onClick={() => handleClick("C")} />
        </div>
      </div>
    </div>
  );
};

export default Calculator;
// app/components/Display.js
const Display = ({ value }) => {
  return (
    <div className="flex justify-end items-center w-full h-20 bg-gray-800 text-white p-4 rounded mb-2">
      {value}
    </div>
  );
};

export default Display;
// app/components/Button.js
const Button = ({ value, onClick }) => {
  return (
    <button
      className="bg-gray-800 text-white w-20 h-20 m-2 rounded"
      onClick={onClick}
    >
      {value}
    </button>
  );
};

export default Button;

In the Calculator component, we've added an extra div wrapper around the "C" button with the width of a single button to ensure it aligns to the left.

In the Display component, we've added the mb-2 class to match the padding between the buttons, which is m-2 in the Button component.

This adjustment should resolve the issues you've mentioned.

Follow-up Prompt

The C button is still centered. Align it to the right

Response

I see, my apologies for the confusion earlier. To align the "C" button to the right, you can use justify-end to align it at the end of the available space in the div.

Here's how you can modify your Calculator component:

// app/components/Calculator.js
import { useState } from 'react';
import { add, subtract, multiply, divide } from '../utils/utils';
import Display from './Display';
import Button from './Button';

const Calculator = () => {
  const [display, setDisplay] = useState("");
  const [firstOperand, setFirstOperand] = useState(null);
  const [currentOperation, setCurrentOperation] = useState(null);

  const buttons = [
    '7', '8', '9', '/',
    '4', '5', '6', '*',
    '1', '2', '3', '-',
    '0', '.', '=', '+'
  ];

  const handleClick = /* Your handleClick function */

  const performCalculation = /* Your performCalculation function */

  return (
    <div className="p-10 flex flex-col items-center w-full max-w-lg mx-auto">
      <Display value={display} />
      <div className="flex flex-wrap justify-center">
        {buttons.map((button, index) => (
          <Button key={index} value={button} onClick={() => handleClick(button)} />
        ))}
        <div className="w-full flex justify-end">
          <Button value="C" onClick={() => handleClick("C")} />
        </div>
      </div>
    </div>
  );
};

export default Calculator;

In this updated Calculator component, the div wrapping the "C" button has been given the classes w-full and flex justify-end. w-full ensures that the div takes up the full available width, while flex justify-end aligns the "C" button to the right.

This change should position the "C" button to the right.

Follow-up Prompt

Two more issues: 1. subtraction always shows the value 0 after clicking =. 2. Division always shows the value 1 after clicking =. Please fix.

Response

It seems like there might be an issue with the performCalculation function and how it's using the display and firstOperand states to calculate the results.

One potential issue might be the display.slice(firstOperand.length + 1) call in your performCalculation function. Since firstOperand is a number and display is a string, firstOperand.length will be undefined, and undefined + 1 is NaN, so display.slice(NaN) will return the original display string, not the second operand as you might be expecting.

To fix this, you might want to convert firstOperand to a string when determining the starting index for the slice:

const secondOperandStartIndex = firstOperand.toString().length + 1;

Then, use secondOperandStartIndex when calling display.slice:

parseFloat(display.slice(secondOperandStartIndex))

Here's how you can adjust your performCalculation function:

const performCalculation = () => {
  if (firstOperand !== null && currentOperation) {
    const secondOperandStartIndex = firstOperand.toString().length + 1;
    const secondOperand = parseFloat(display.slice(secondOperandStartIndex));

    switch (currentOperation) {
      case '+':
        return add(parseFloat(firstOperand), secondOperand);
      case '-':
        return subtract(parseFloat(firstOperand), secondOperand);
      case '*':
        return multiply(parseFloat(firstOperand), secondOperand);
      case '/':
        try {
          return divide(parseFloat(firstOperand), secondOperand);
        } catch (e) {
          setDisplay(e.message);
        }
        break;
      default:
        break;
    }
  }
};

Try making these adjustments and see if they fix the issues you're experiencing with subtraction and division.

About

A calculator made using chatgpt-4 with some prompt engineering techniques and minor adjustments

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •