├── src
├── vite-env.d.ts
├── index.css
├── main.tsx
├── types
│ └── calculator.ts
├── components
│ ├── Display.tsx
│ ├── CalculatorButton.tsx
│ ├── ModeSelector.tsx
│ ├── History.tsx
│ └── calculators
│ │ ├── StandardCalculator.tsx
│ │ ├── GraphingCalculator.tsx
│ │ ├── ConversionCalculator.tsx
│ │ ├── FinancialCalculator.tsx
│ │ └── ScientificCalculator.tsx
├── hooks
│ └── useCalculator.ts
└── App.tsx
├── .bolt
├── config.json
└── prompt
├── public
├── black_circle_360x360.png
└── white_circle_360x360.png
├── postcss.config.js
├── tsconfig.json
├── tailwind.config.js
├── vite.config.ts
├── .gitignore
├── tsconfig.node.json
├── index.html
├── tsconfig.app.json
├── eslint.config.js
├── package.json
├── LICENSE
└── README.md
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.bolt/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "template": "bolt-vite-react-ts"
3 | }
4 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/public/black_circle_360x360.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seehiong/swift-calc/HEAD/public/black_circle_360x360.png
--------------------------------------------------------------------------------
/public/white_circle_360x360.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seehiong/swift-calc/HEAD/public/white_circle_360x360.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | optimizeDeps: {
8 | exclude: ['lucide-react'],
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App.tsx';
4 | import './index.css';
5 |
6 | createRoot(document.getElementById('root')!).render(
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/.bolt/prompt:
--------------------------------------------------------------------------------
1 | For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
2 |
3 | By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
4 |
5 | Use icons from lucide-react for logos.
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .env
26 |
27 | # Local Netlify folder
28 | .netlify
29 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SwiftCalc | Professional Calculator Suite
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"]
24 | }
25 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js';
2 | import globals from 'globals';
3 | import reactHooks from 'eslint-plugin-react-hooks';
4 | import reactRefresh from 'eslint-plugin-react-refresh';
5 | import tseslint from 'typescript-eslint';
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | }
28 | );
29 |
--------------------------------------------------------------------------------
/src/types/calculator.ts:
--------------------------------------------------------------------------------
1 | export type CalculatorMode = 'standard' | 'scientific' | 'graphing' | 'financial' | 'conversion';
2 |
3 | export interface CalculationHistory {
4 | id: string;
5 | expression: string;
6 | result: string;
7 | timestamp: Date;
8 | mode: CalculatorMode;
9 | }
10 |
11 | export interface FinancialCalculation {
12 | type: 'loan' | 'compound' | 'investment';
13 | principal?: number;
14 | rate?: number;
15 | time?: number;
16 | payment?: number;
17 | futureValue?: number;
18 | presentValue?: number;
19 | }
20 |
21 | export interface ConversionUnit {
22 | name: string;
23 | symbol: string;
24 | factor: number;
25 | }
26 |
27 | export interface ConversionCategory {
28 | name: string;
29 | units: ConversionUnit[];
30 | }
31 |
32 | export interface GraphFunction {
33 | id: string;
34 | expression: string;
35 | color: string;
36 | visible: boolean;
37 | }
--------------------------------------------------------------------------------
/src/components/Display.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface DisplayProps {
4 | value: string;
5 | memory: number;
6 | expression?: string;
7 | }
8 |
9 | export const Display: React.FC = ({ value, memory, expression }) => {
10 | return (
11 |
12 | {expression && (
13 |
14 | {expression}
15 |
16 | )}
17 |
18 |
19 | {value}
20 |
21 | {memory !== 0 && (
22 |
23 | M: {memory}
24 |
25 | )}
26 |
27 |
28 | );
29 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "swift-calc",
3 | "private": true,
4 | "version": "1.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "lucide-react": "^0.344.0",
14 | "react": "^18.3.1",
15 | "react-dom": "^18.3.1",
16 | "mathjs": "^12.4.0",
17 | "chart.js": "^4.4.0",
18 | "react-chartjs-2": "^5.2.0",
19 | "chartjs-adapter-date-fns": "^3.0.0"
20 | },
21 | "devDependencies": {
22 | "@eslint/js": "^9.9.1",
23 | "@types/react": "^18.3.5",
24 | "@types/react-dom": "^18.3.0",
25 | "@vitejs/plugin-react": "^4.3.1",
26 | "autoprefixer": "^10.4.18",
27 | "eslint": "^9.9.1",
28 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
29 | "eslint-plugin-react-refresh": "^0.4.11",
30 | "globals": "^15.9.0",
31 | "postcss": "^8.4.35",
32 | "tailwindcss": "^3.4.1",
33 | "typescript": "^5.5.3",
34 | "typescript-eslint": "^8.3.0",
35 | "vite": "^5.4.2"
36 | }
37 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 SwiftCalc
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/components/CalculatorButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface CalculatorButtonProps {
4 | value: string;
5 | onClick: (value: string) => void;
6 | className?: string;
7 | children?: React.ReactNode;
8 | disabled?: boolean;
9 | variant?: 'default' | 'operator' | 'equals' | 'clear' | 'memory' | 'function';
10 | }
11 |
12 | export const CalculatorButton: React.FC = ({
13 | value,
14 | onClick,
15 | className = '',
16 | children,
17 | disabled = false,
18 | variant = 'default',
19 | }) => {
20 | const baseClasses = 'h-12 rounded-lg font-medium transition-all duration-200 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed';
21 |
22 | const variantClasses = {
23 | default: 'bg-white/80 hover:bg-white text-gray-800 border border-gray-200 hover:border-gray-300 shadow-sm hover:shadow-md',
24 | operator: 'bg-blue-500 hover:bg-blue-600 text-white shadow-md hover:shadow-lg',
25 | equals: 'bg-green-500 hover:bg-green-600 text-white shadow-md hover:shadow-lg',
26 | clear: 'bg-red-500 hover:bg-red-600 text-white shadow-md hover:shadow-lg',
27 | memory: 'bg-purple-500 hover:bg-purple-600 text-white shadow-md hover:shadow-lg',
28 | function: 'bg-indigo-500 hover:bg-indigo-600 text-white shadow-md hover:shadow-lg text-sm',
29 | };
30 |
31 | return (
32 |
39 | );
40 | };
--------------------------------------------------------------------------------
/src/components/ModeSelector.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Calculator, Sigma, TrendingUp, DollarSign, ArrowLeftRight } from 'lucide-react';
3 | import { CalculatorMode } from '../types/calculator';
4 |
5 | interface ModeSelectorProps {
6 | currentMode: CalculatorMode;
7 | onModeChange: (mode: CalculatorMode) => void;
8 | }
9 |
10 | const modes: { id: CalculatorMode; label: string; icon: React.ReactNode }[] = [
11 | { id: 'standard', label: 'Standard', icon: },
12 | { id: 'scientific', label: 'Scientific', icon: },
13 | { id: 'graphing', label: 'Graphing', icon: },
14 | { id: 'financial', label: 'Financial', icon: },
15 | { id: 'conversion', label: 'Convert', icon: },
16 | ];
17 |
18 | export const ModeSelector: React.FC = ({ currentMode, onModeChange }) => {
19 | return (
20 |
21 | {modes.map((mode) => (
22 |
34 | ))}
35 |
36 | );
37 | };
--------------------------------------------------------------------------------
/src/hooks/useCalculator.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from 'react';
2 | import { evaluate } from 'mathjs';
3 | import { CalculationHistory, CalculatorMode } from '../types/calculator';
4 |
5 | export const useCalculator = () => {
6 | const [display, setDisplay] = useState('0');
7 | const [memory, setMemory] = useState(0);
8 | const [lastResult, setLastResult] = useState('0');
9 | const [history, setHistory] = useState([]);
10 | const [mode, setMode] = useState('standard');
11 | const [isDarkMode, setIsDarkMode] = useState(false);
12 |
13 | const addToHistory = useCallback((expression: string, result: string) => {
14 | const newEntry: CalculationHistory = {
15 | id: Date.now().toString(),
16 | expression,
17 | result,
18 | timestamp: new Date(),
19 | mode,
20 | };
21 | setHistory(prev => [newEntry, ...prev.slice(0, 49)]); // Keep last 50 entries
22 | }, [mode]);
23 |
24 | // Calculate function that automatically adds to history (for simple calculations)
25 | const calculate = useCallback((expression: string) => {
26 | try {
27 | console.log('Calculating:', expression);
28 | const result = evaluate(expression);
29 | console.log('Result:', result);
30 |
31 | let resultStr: string;
32 | if (typeof result === 'number') {
33 | if (Number.isInteger(result)) {
34 | // Format integers with commas for readability
35 | resultStr = result.toLocaleString();
36 | } else {
37 | // Format decimals, remove trailing zeros, then add commas
38 | const formatted = result.toFixed(8).replace(/\.?0+$/, '');
39 | const num = parseFloat(formatted);
40 | resultStr = num.toLocaleString();
41 | }
42 | } else {
43 | resultStr = result.toString();
44 | }
45 |
46 | setDisplay(resultStr);
47 | setLastResult(resultStr);
48 | return resultStr;
49 | } catch (error) {
50 | console.error('Calculation error:', error);
51 | setDisplay('Error');
52 | return 'Error';
53 | }
54 | }, []);
55 |
56 | // Calculate function that doesn't add to history (for manual history control)
57 | const calculateOnly = useCallback((expression: string) => {
58 | try {
59 | console.log('Calculating:', expression);
60 | const result = evaluate(expression);
61 | console.log('Result:', result);
62 |
63 | let resultStr: string;
64 | if (typeof result === 'number') {
65 | if (Number.isInteger(result)) {
66 | resultStr = result.toLocaleString();
67 | } else {
68 | const formatted = result.toFixed(8).replace(/\.?0+$/, '');
69 | const num = parseFloat(formatted);
70 | resultStr = num.toLocaleString();
71 | }
72 | } else {
73 | resultStr = result.toString();
74 | }
75 |
76 | return resultStr;
77 | } catch (error) {
78 | console.error('Calculation error:', error);
79 | return 'Error';
80 | }
81 | }, []);
82 |
83 | const clearDisplay = useCallback(() => {
84 | setDisplay('0');
85 | }, []);
86 |
87 | const clearAll = useCallback(() => {
88 | setDisplay('0');
89 | }, []);
90 |
91 | const memoryAdd = useCallback(() => {
92 | setMemory(prev => prev + parseFloat(display || '0'));
93 | }, [display]);
94 |
95 | const memorySubtract = useCallback(() => {
96 | setMemory(prev => prev - parseFloat(display || '0'));
97 | }, [display]);
98 |
99 | const memoryRecall = useCallback(() => {
100 | setDisplay(memory.toString());
101 | }, [memory]);
102 |
103 | const memoryClear = useCallback(() => {
104 | setMemory(0);
105 | }, []);
106 |
107 | const clearHistory = useCallback(() => {
108 | setHistory([]);
109 | }, []);
110 |
111 | const toggleDarkMode = useCallback(() => {
112 | setIsDarkMode(prev => !prev);
113 | }, []);
114 |
115 | const recallLastResult = useCallback(() => {
116 | setDisplay(lastResult);
117 | }, [lastResult]);
118 |
119 | return {
120 | display,
121 | setDisplay,
122 | memory,
123 | history,
124 | mode,
125 | setMode,
126 | isDarkMode,
127 | lastResult,
128 | setLastResult,
129 | calculate,
130 | calculateOnly,
131 | clearDisplay,
132 | clearAll,
133 | memoryAdd,
134 | memorySubtract,
135 | memoryRecall,
136 | memoryClear,
137 | toggleDarkMode,
138 | addToHistory,
139 | clearHistory,
140 | recallLastResult,
141 | };
142 | };
--------------------------------------------------------------------------------
/src/components/History.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { History as HistoryIcon, Trash2, Copy, Check } from 'lucide-react';
3 | import { CalculationHistory } from '../types/calculator';
4 |
5 | interface HistoryProps {
6 | history: CalculationHistory[];
7 | onClearHistory: () => void;
8 | onSelectHistoryItem: (item: CalculationHistory) => void;
9 | }
10 |
11 | export const History: React.FC = ({
12 | history,
13 | onClearHistory,
14 | onSelectHistoryItem
15 | }) => {
16 | const [expandedItem, setExpandedItem] = useState(null);
17 | const [copiedItem, setCopiedItem] = useState(null);
18 |
19 | const copyToClipboard = async (text: string, itemId: string) => {
20 | try {
21 | await navigator.clipboard.writeText(text);
22 | setCopiedItem(itemId);
23 | setTimeout(() => setCopiedItem(null), 2000);
24 | } catch (err) {
25 | console.error('Failed to copy text: ', err);
26 | }
27 | };
28 |
29 | if (history.length === 0) {
30 | return (
31 |
32 |
33 |
34 |
History
35 |
36 |
37 |
38 |
No calculations yet
39 |
Your calculation history will appear here
40 |
41 |
42 | );
43 | }
44 |
45 | return (
46 |
47 |
48 |
49 |
50 |
History
51 | ({history.length})
52 | (Newest first)
53 |
54 |
61 |
62 |
63 |
64 | {history.map((item) => (
65 |
69 |
70 |
71 |
82 |
83 |
94 |
95 |
96 | {item.mode.toUpperCase()}
97 |
98 |
99 | {item.timestamp.toLocaleString([], {
100 | month: 'short',
101 | day: 'numeric',
102 | hour: '2-digit',
103 | minute: '2-digit'
104 | })}
105 |
106 |
107 |
108 |
109 |
110 |
111 | {/* Expand/Collapse button for long expressions */}
112 | {item.expression.length > 50 && (
113 |
119 | )}
120 |
121 | ))}
122 |
123 |
124 | );
125 | };
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Moon, Sun, Calculator as CalcIcon } from 'lucide-react';
2 | import { useCalculator } from './hooks/useCalculator';
3 | import { ModeSelector } from './components/ModeSelector';
4 | import { StandardCalculator } from './components/calculators/StandardCalculator';
5 | import { ScientificCalculator } from './components/calculators/ScientificCalculator';
6 | import { GraphingCalculator } from './components/calculators/GraphingCalculator';
7 | import { FinancialCalculator } from './components/calculators/FinancialCalculator';
8 | import { ConversionCalculator } from './components/calculators/ConversionCalculator';
9 | import { History } from './components/History';
10 |
11 | function App() {
12 | const calculator = useCalculator();
13 | const {
14 | display,
15 | setDisplay,
16 | memory,
17 | history,
18 | mode,
19 | setMode,
20 | isDarkMode,
21 | calculate,
22 | clearDisplay,
23 | clearAll,
24 | memoryAdd,
25 | memorySubtract,
26 | memoryRecall,
27 | memoryClear,
28 | toggleDarkMode,
29 | clearHistory,
30 | recallLastResult,
31 | addToHistory,
32 | } = calculator;
33 |
34 | const handleHistoryItemSelect = (item: any) => {
35 | setDisplay(item.result);
36 | };
37 |
38 | return (
39 |
43 |
44 | {/* Header */}
45 |
72 |
73 | {/* Mode Selector */}
74 |
75 |
76 |
77 | {/* Calculator */}
78 |
79 | {mode === 'standard' && (
80 |
92 | )}
93 |
94 | {mode === 'scientific' && (
95 |
105 | )}
106 |
107 | {mode === 'graphing' && (
108 |
109 | )}
110 |
111 | {mode === 'financial' && (
112 |
113 | )}
114 |
115 | {mode === 'conversion' && (
116 |
117 | )}
118 |
119 |
120 | {/* History Panel */}
121 |
122 |
127 |
128 |
129 |
130 | {/* Footer */}
131 |
135 |
136 |
137 | );
138 | }
139 |
140 | export default App;
--------------------------------------------------------------------------------
/src/components/calculators/StandardCalculator.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { CalculatorButton } from '../CalculatorButton';
3 | import { Display } from '../Display';
4 |
5 | interface StandardCalculatorProps {
6 | display: string;
7 | setDisplay: (value: string) => void;
8 | memory: number;
9 | calculate: (expression: string) => string;
10 | clearDisplay: () => void;
11 | clearAll: () => void;
12 | memoryAdd: () => void;
13 | memorySubtract: () => void;
14 | memoryRecall: () => void;
15 | memoryClear: () => void;
16 | }
17 |
18 | export const StandardCalculator: React.FC = ({
19 | display,
20 | setDisplay,
21 | memory,
22 | calculate,
23 | clearDisplay,
24 | clearAll,
25 | memoryAdd,
26 | memorySubtract,
27 | memoryRecall,
28 | memoryClear,
29 | }) => {
30 | const [expression, setExpression] = useState('');
31 | const [waitingForOperand, setWaitingForOperand] = useState(false);
32 |
33 | const inputNumber = (num: string) => {
34 | if (waitingForOperand) {
35 | setDisplay(num);
36 | setWaitingForOperand(false);
37 | } else {
38 | setDisplay(display === '0' ? num : display + num);
39 | }
40 | };
41 |
42 | const inputOperator = (nextOperator: string) => {
43 | const inputValue = parseFloat(display);
44 |
45 | if (expression === '') {
46 | setExpression(display + ' ' + nextOperator + ' ');
47 | } else {
48 | const result = calculate(expression + display);
49 | setExpression(result + ' ' + nextOperator + ' ');
50 | }
51 |
52 | setWaitingForOperand(true);
53 | };
54 |
55 | const performCalculation = () => {
56 | if (expression !== '') {
57 | const result = calculate(expression + display);
58 | setDisplay(result);
59 | setExpression('');
60 | setWaitingForOperand(true);
61 | }
62 | };
63 |
64 | const inputDecimal = () => {
65 | if (waitingForOperand) {
66 | setDisplay('0.');
67 | setWaitingForOperand(false);
68 | } else if (display.indexOf('.') === -1) {
69 | setDisplay(display + '.');
70 | }
71 | };
72 |
73 | const toggleSign = () => {
74 | if (display !== '0') {
75 | setDisplay(display.startsWith('-') ? display.slice(1) : '-' + display);
76 | }
77 | };
78 |
79 | const percentage = () => {
80 | const value = parseFloat(display) / 100;
81 | setDisplay(value.toString());
82 | };
83 |
84 | const handleClear = () => {
85 | clearDisplay();
86 | setExpression('');
87 | setWaitingForOperand(false);
88 | };
89 |
90 | const handleAllClear = () => {
91 | clearAll();
92 | setExpression('');
93 | setWaitingForOperand(false);
94 | };
95 |
96 | return (
97 |
98 |
99 |
100 |
101 | {/* Row 1 */}
102 | AC
103 | C
104 | ±
105 | inputOperator('/')} variant="operator">÷
106 |
107 | {/* Row 2 */}
108 | MC
109 | MR
110 | M+
111 | inputOperator('*')} variant="operator">×
112 |
113 | {/* Row 3 */}
114 | inputNumber('7')}>7
115 | inputNumber('8')}>8
116 | inputNumber('9')}>9
117 | inputOperator('-')} variant="operator">−
118 |
119 | {/* Row 4 */}
120 | inputNumber('4')}>4
121 | inputNumber('5')}>5
122 | inputNumber('6')}>6
123 | inputOperator('+')} variant="operator">+
124 |
125 | {/* Row 5 */}
126 | inputNumber('1')}>1
127 | inputNumber('2')}>2
128 | inputNumber('3')}>3
129 | %
130 |
131 | {/* Row 6 */}
132 | inputNumber('0')} className="col-span-2">0
133 | .
134 |
135 | {/* Equals button */}
136 | =
137 |
138 |
139 | );
140 | };
--------------------------------------------------------------------------------
/src/components/calculators/GraphingCalculator.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import { Line } from 'react-chartjs-2';
3 | import { evaluate } from 'mathjs';
4 | import {
5 | Chart as ChartJS,
6 | CategoryScale,
7 | LinearScale,
8 | PointElement,
9 | LineElement,
10 | Title,
11 | Tooltip,
12 | Legend,
13 | ChartOptions,
14 | } from 'chart.js';
15 | import { GraphFunction } from '../../types/calculator';
16 | import { Plus, Eye, EyeOff, Trash2 } from 'lucide-react';
17 |
18 | ChartJS.register(
19 | CategoryScale,
20 | LinearScale,
21 | PointElement,
22 | LineElement,
23 | Title,
24 | Tooltip,
25 | Legend
26 | );
27 |
28 | interface GraphingCalculatorProps {
29 | calculate: (expression: string) => string;
30 | }
31 |
32 | export const GraphingCalculator: React.FC = ({ calculate }) => {
33 | const [functions, setFunctions] = useState([
34 | { id: '1', expression: 'x^2', color: '#3B82F6', visible: true },
35 | ]);
36 | const [newFunction, setNewFunction] = useState('');
37 | const [xMin, setXMin] = useState(-10);
38 | const [xMax, setXMax] = useState(10);
39 | const [yMin, setYMin] = useState(-10);
40 | const [yMax, setYMax] = useState(10);
41 |
42 | const colors = ['#3B82F6', '#EF4444', '#10B981', '#F59E0B', '#8B5CF6', '#EC4899'];
43 |
44 | const generateGraphData = () => {
45 | const step = (xMax - xMin) / 400;
46 | const xValues: number[] = [];
47 | for (let x = xMin; x <= xMax; x += step) {
48 | xValues.push(x);
49 | }
50 |
51 | const datasets = functions
52 | .filter(func => func.visible)
53 | .map((func, index) => {
54 | const yValues = xValues.map(x => {
55 | try {
56 | const expression = func.expression.replace(/x/g, `(${x})`);
57 | const result = evaluate(expression);
58 | return typeof result === 'number' && isFinite(result) ? result : null;
59 | } catch {
60 | return null;
61 | }
62 | });
63 |
64 | return {
65 | label: `y = ${func.expression}`,
66 | data: yValues,
67 | borderColor: func.color,
68 | backgroundColor: func.color + '20',
69 | borderWidth: 2,
70 | fill: false,
71 | tension: 0,
72 | pointRadius: 0,
73 | pointHoverRadius: 4,
74 | };
75 | });
76 |
77 | return {
78 | labels: xValues.map(x => x.toFixed(2)),
79 | datasets,
80 | };
81 | };
82 |
83 | const chartOptions: ChartOptions<'line'> = {
84 | responsive: true,
85 | maintainAspectRatio: false,
86 | plugins: {
87 | legend: {
88 | position: 'top' as const,
89 | },
90 | tooltip: {
91 | mode: 'index',
92 | intersect: false,
93 | },
94 | },
95 | scales: {
96 | x: {
97 | type: 'linear',
98 | position: 'center',
99 | min: xMin,
100 | max: xMax,
101 | grid: {
102 | color: 'rgba(0, 0, 0, 0.1)',
103 | },
104 | ticks: {
105 | stepSize: (xMax - xMin) / 10,
106 | },
107 | },
108 | y: {
109 | type: 'linear',
110 | position: 'center',
111 | min: yMin,
112 | max: yMax,
113 | grid: {
114 | color: 'rgba(0, 0, 0, 0.1)',
115 | },
116 | ticks: {
117 | stepSize: (yMax - yMin) / 10,
118 | },
119 | },
120 | },
121 | interaction: {
122 | mode: 'nearest',
123 | axis: 'x',
124 | intersect: false,
125 | },
126 | };
127 |
128 | const addFunction = () => {
129 | if (newFunction.trim()) {
130 | const newFunc: GraphFunction = {
131 | id: Date.now().toString(),
132 | expression: newFunction.trim(),
133 | color: colors[functions.length % colors.length],
134 | visible: true,
135 | };
136 | setFunctions([...functions, newFunc]);
137 | setNewFunction('');
138 | }
139 | };
140 |
141 | const removeFunction = (id: string) => {
142 | setFunctions(functions.filter(func => func.id !== id));
143 | };
144 |
145 | const toggleVisibility = (id: string) => {
146 | setFunctions(functions.map(func =>
147 | func.id === id ? { ...func, visible: !func.visible } : func
148 | ));
149 | };
150 |
151 | const updateExpression = (id: string, expression: string) => {
152 | setFunctions(functions.map(func =>
153 | func.id === id ? { ...func, expression } : func
154 | ));
155 | };
156 |
157 | const resetView = () => {
158 | setXMin(-10);
159 | setXMax(10);
160 | setYMin(-10);
161 | setYMax(10);
162 | };
163 |
164 | return (
165 |
166 | {/* Function Input */}
167 |
168 |
169 |
setNewFunction(e.target.value)}
173 | placeholder="Enter function (e.g., x^2, sin(x), 2*x + 1)"
174 | className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
175 | onKeyPress={(e) => e.key === 'Enter' && addFunction()}
176 | />
177 |
184 |
185 |
186 | {/* Function List */}
187 |
188 | {functions.map((func) => (
189 |
190 |
194 |
updateExpression(func.id, e.target.value)}
198 | className="flex-1 px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
199 | />
200 |
206 |
212 |
213 | ))}
214 |
215 |
216 |
217 | {/* View Controls */}
218 |
219 |
257 |
263 |
264 |
265 | {/* Graph */}
266 |
271 |
272 | );
273 | };
--------------------------------------------------------------------------------
/src/components/calculators/ConversionCalculator.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { ConversionCategory } from '../../types/calculator';
3 |
4 | interface ConversionCalculatorProps {
5 | calculate: (expression: string) => string;
6 | }
7 |
8 | const conversionCategories: ConversionCategory[] = [
9 | {
10 | name: 'Length',
11 | units: [
12 | { name: 'Meter', symbol: 'm', factor: 1 },
13 | { name: 'Kilometer', symbol: 'km', factor: 1000 },
14 | { name: 'Centimeter', symbol: 'cm', factor: 0.01 },
15 | { name: 'Millimeter', symbol: 'mm', factor: 0.001 },
16 | { name: 'Inch', symbol: 'in', factor: 0.0254 },
17 | { name: 'Foot', symbol: 'ft', factor: 0.3048 },
18 | { name: 'Yard', symbol: 'yd', factor: 0.9144 },
19 | { name: 'Mile', symbol: 'mi', factor: 1609.34 },
20 | ],
21 | },
22 | {
23 | name: 'Weight',
24 | units: [
25 | { name: 'Kilogram', symbol: 'kg', factor: 1 },
26 | { name: 'Gram', symbol: 'g', factor: 0.001 },
27 | { name: 'Pound', symbol: 'lb', factor: 0.453592 },
28 | { name: 'Ounce', symbol: 'oz', factor: 0.0283495 },
29 | { name: 'Stone', symbol: 'st', factor: 6.35029 },
30 | { name: 'Ton', symbol: 't', factor: 1000 },
31 | ],
32 | },
33 | {
34 | name: 'Temperature',
35 | units: [
36 | { name: 'Celsius', symbol: '°C', factor: 1 },
37 | { name: 'Fahrenheit', symbol: '°F', factor: 1 },
38 | { name: 'Kelvin', symbol: 'K', factor: 1 },
39 | ],
40 | },
41 | {
42 | name: 'Area',
43 | units: [
44 | { name: 'Square Meter', symbol: 'm²', factor: 1 },
45 | { name: 'Square Kilometer', symbol: 'km²', factor: 1000000 },
46 | { name: 'Square Centimeter', symbol: 'cm²', factor: 0.0001 },
47 | { name: 'Square Inch', symbol: 'in²', factor: 0.00064516 },
48 | { name: 'Square Foot', symbol: 'ft²', factor: 0.092903 },
49 | { name: 'Acre', symbol: 'acre', factor: 4046.86 },
50 | { name: 'Hectare', symbol: 'ha', factor: 10000 },
51 | ],
52 | },
53 | {
54 | name: 'Volume',
55 | units: [
56 | { name: 'Liter', symbol: 'L', factor: 1 },
57 | { name: 'Milliliter', symbol: 'mL', factor: 0.001 },
58 | { name: 'Gallon (US)', symbol: 'gal', factor: 3.78541 },
59 | { name: 'Quart (US)', symbol: 'qt', factor: 0.946353 },
60 | { name: 'Pint (US)', symbol: 'pt', factor: 0.473176 },
61 | { name: 'Cup (US)', symbol: 'cup', factor: 0.236588 },
62 | { name: 'Fluid Ounce (US)', symbol: 'fl oz', factor: 0.0295735 },
63 | ],
64 | },
65 | {
66 | name: 'Time',
67 | units: [
68 | { name: 'Second', symbol: 's', factor: 1 },
69 | { name: 'Minute', symbol: 'min', factor: 60 },
70 | { name: 'Hour', symbol: 'h', factor: 3600 },
71 | { name: 'Day', symbol: 'd', factor: 86400 },
72 | { name: 'Week', symbol: 'wk', factor: 604800 },
73 | { name: 'Month', symbol: 'mo', factor: 2629746 },
74 | { name: 'Year', symbol: 'yr', factor: 31556952 },
75 | ],
76 | },
77 | ];
78 |
79 | export const ConversionCalculator: React.FC = () => {
80 | const [selectedCategory, setSelectedCategory] = useState(conversionCategories[0]);
81 | const [fromUnit, setFromUnit] = useState(selectedCategory.units[0]);
82 | const [toUnit, setToUnit] = useState(selectedCategory.units[1]);
83 | const [inputValue, setInputValue] = useState('1');
84 | const [result, setResult] = useState('');
85 |
86 | const convertTemperature = (value: number, from: string, to: string): number => {
87 | // Convert to Celsius first
88 | let celsius: number;
89 | if (from === '°C') {
90 | celsius = value;
91 | } else if (from === '°F') {
92 | celsius = (value - 32) * 5/9;
93 | } else { // Kelvin
94 | celsius = value - 273.15;
95 | }
96 |
97 | // Convert from Celsius to target
98 | if (to === '°C') {
99 | return celsius;
100 | } else if (to === '°F') {
101 | return celsius * 9/5 + 32;
102 | } else { // Kelvin
103 | return celsius + 273.15;
104 | }
105 | };
106 |
107 | const performConversion = () => {
108 | const value = parseFloat(inputValue);
109 | if (isNaN(value)) {
110 | setResult('Invalid input');
111 | return;
112 | }
113 |
114 | if (selectedCategory.name === 'Temperature') {
115 | const converted = convertTemperature(value, fromUnit.symbol, toUnit.symbol);
116 | setResult(converted.toFixed(4).replace(/\.?0+$/, ''));
117 | } else {
118 | // Standard unit conversion using base factors
119 | const baseValue = value * fromUnit.factor;
120 | const converted = baseValue / toUnit.factor;
121 | setResult(converted.toFixed(8).replace(/\.?0+$/, ''));
122 | }
123 | };
124 |
125 | const handleCategoryChange = (category: ConversionCategory) => {
126 | setSelectedCategory(category);
127 | setFromUnit(category.units[0]);
128 | setToUnit(category.units[1] || category.units[0]);
129 | setResult('');
130 | };
131 |
132 | React.useEffect(() => {
133 | if (inputValue && fromUnit && toUnit) {
134 | performConversion();
135 | }
136 | }, [inputValue, fromUnit, toUnit]);
137 |
138 | const swapUnits = () => {
139 | const temp = fromUnit;
140 | setFromUnit(toUnit);
141 | setToUnit(temp);
142 | };
143 |
144 | return (
145 |
146 | {/* Category Selection */}
147 |
148 |
Conversion Categories
149 |
150 | {conversionCategories.map((category) => (
151 |
162 | ))}
163 |
164 |
165 |
166 | {/* Conversion Interface */}
167 |
168 |
{selectedCategory.name} Conversion
169 |
170 |
171 | {/* From Unit */}
172 |
173 |
174 | setInputValue(e.target.value)}
178 | className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 mb-2"
179 | placeholder="Enter value"
180 | />
181 |
192 |
193 |
194 | {/* Swap Button */}
195 |
196 |
202 |
203 |
204 | {/* To Unit */}
205 |
206 |
207 |
208 |
209 | {result || '0'}
210 |
211 |
212 |
223 |
224 |
225 |
226 | {/* Conversion Formula */}
227 | {result && (
228 |
229 |
230 | Result: {inputValue} {fromUnit.symbol} = {result} {toUnit.symbol}
231 |
232 |
233 | )}
234 |
235 | {/* Quick Conversions */}
236 |
237 |
Quick Conversions
238 |
239 | {[1, 10, 100, 1000].map((quickValue) => {
240 | const quickResult = selectedCategory.name === 'Temperature'
241 | ? convertTemperature(quickValue, fromUnit.symbol, toUnit.symbol)
242 | : (quickValue * fromUnit.factor) / toUnit.factor;
243 |
244 | return (
245 |
252 | );
253 | })}
254 |
255 |
256 |
257 |
258 | );
259 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftCalc
2 |
3 | A comprehensive, professional-grade calculator suite built with React, TypeScript, and Tailwind CSS. SwiftCalc provides multiple calculator modes including Standard, Scientific, Graphing, Financial, and Unit Conversion calculators - all in one swift, elegant interface.
4 |
5 | 
6 |
7 | ## ✨ Features
8 |
9 | ### 🧮 Calculator Modes
10 | - **Standard Calculator**: Basic arithmetic operations with memory functions
11 | - **Scientific Calculator**: Advanced mathematical functions, trigonometry, logarithms, and more
12 | - **Graphing Calculator**: Plot mathematical functions with customizable viewing windows
13 | - **Financial Calculator**: Loan payments, compound interest, and investment calculations
14 | - **Unit Converter**: Convert between various units (length, weight, temperature, area, volume, time)
15 |
16 | ### 🎨 User Experience
17 | - **Dark/Light Mode**: Toggle between themes for comfortable viewing
18 | - **Calculation History**: Track and reuse previous calculations
19 | - **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices
20 | - **Professional UI**: Clean, modern interface with smooth animations
21 | - **Memory Functions**: Store and recall values across calculations
22 |
23 | ### 🔧 Technical Features
24 | - **Real-time Graphing**: Interactive function plotting with Chart.js
25 | - **Mathematical Expression Parsing**: Powered by Math.js for accurate calculations
26 | - **Type Safety**: Full TypeScript implementation
27 | - **Performance Optimized**: Efficient rendering and state management
28 | - **Accessibility**: Keyboard navigation and screen reader support
29 |
30 | ## 🚀 Getting Started
31 |
32 | ### Prerequisites
33 | - Node.js (version 16 or higher)
34 | - npm or yarn package manager
35 |
36 | ### Installation
37 |
38 | 1. **Clone the repository**
39 | ```bash
40 | git clone https://github.com/seehiong/swift-calc.git
41 | cd swift-calc
42 | ```
43 |
44 | 2. **Install dependencies**
45 | ```bash
46 | npm install
47 | ```
48 |
49 | 3. **Start the development server**
50 | ```bash
51 | npm run dev
52 | ```
53 |
54 | 4. **Open your browser**
55 | Navigate to `http://localhost:5173` to view the application.
56 |
57 | ### Building for Production
58 |
59 | ```bash
60 | # Build the application
61 | npm run build
62 |
63 | # Preview the production build
64 | npm run preview
65 | ```
66 |
67 | ## 📱 Usage Guide
68 |
69 | ### Standard Calculator
70 | - Basic arithmetic operations (+, -, ×, ÷)
71 | - Memory functions (MC, MR, M+, M-)
72 | - Percentage calculations
73 | - Sign toggle and decimal operations
74 |
75 | ### Scientific Calculator
76 | - Trigonometric functions (sin, cos, tan) with degree/radian modes
77 | - Logarithmic functions (log, ln)
78 | - Power and root operations
79 | - Constants (π, e)
80 | - Factorial and inverse functions
81 |
82 | ### Graphing Calculator
83 | - Plot multiple functions simultaneously
84 | - Customizable viewing window (X/Y min/max)
85 | - Function visibility toggle
86 | - Color-coded function lines
87 | - Interactive zoom and pan
88 |
89 | ### Financial Calculator
90 | - **Loan Calculator**: Calculate monthly payments, total interest
91 | - **Compound Interest**: Calculate growth with various compounding frequencies
92 | - **Investment Calculator**: Plan future value with regular contributions
93 |
94 | ### Unit Converter
95 | - **Length**: Meters, kilometers, inches, feet, miles, etc.
96 | - **Weight**: Kilograms, pounds, ounces, stones, etc.
97 | - **Temperature**: Celsius, Fahrenheit, Kelvin
98 | - **Area**: Square meters, acres, hectares, etc.
99 | - **Volume**: Liters, gallons, cups, fluid ounces, etc.
100 | - **Time**: Seconds, minutes, hours, days, years, etc.
101 |
102 | ## 🛠️ Technology Stack
103 |
104 | - **Frontend Framework**: React 18
105 | - **Language**: TypeScript
106 | - **Styling**: Tailwind CSS
107 | - **Mathematical Engine**: Math.js
108 | - **Charting**: Chart.js with React-Chart.js-2
109 | - **Icons**: Lucide React
110 | - **Build Tool**: Vite
111 | - **Linting**: ESLint with TypeScript support
112 |
113 | ## 📁 Project Structure
114 |
115 | ```
116 | src/
117 | ├── components/
118 | │ ├── calculators/
119 | │ │ ├── StandardCalculator.tsx
120 | │ │ ├── ScientificCalculator.tsx
121 | │ │ ├── GraphingCalculator.tsx
122 | │ │ ├── FinancialCalculator.tsx
123 | │ │ └── ConversionCalculator.tsx
124 | │ ├── BoltBadge.tsx
125 | │ ├── CalculatorButton.tsx
126 | │ ├── Display.tsx
127 | │ ├── History.tsx
128 | │ └── ModeSelector.tsx
129 | ├── hooks/
130 | │ └── useCalculator.ts
131 | ├── types/
132 | │ └── calculator.ts
133 | ├── App.tsx
134 | └── main.tsx
135 | ```
136 |
137 | ## 🧮 **Complete Scientific Calculator Button Test Cases**
138 |
139 | ### **Row 1 - Trigonometric & Logarithmic Functions**
140 | | Button | Test Input | Expected Result | Expression Shown |
141 | |--------|------------|-----------------|------------------|
142 | | **sin** | `30` → `sin` → `=` | `0.5` (DEG mode) | `sin(30)` |
143 | | **sin⁻¹** | `INV` → `0.5` → `sin` → `=` | `30` (DEG mode) | `asin(0.5)` |
144 | | **cos** | `60` → `cos` → `=` | `0.5` (DEG mode) | `cos(60)` |
145 | | **cos⁻¹** | `INV` → `0.5` → `cos` → `=` | `60` (DEG mode) | `acos(0.5)` |
146 | | **tan** | `45` → `tan` → `=` | `1` (DEG mode) | `tan(45)` |
147 | | **tan⁻¹** | `INV` → `1` → `tan` → `=` | `45` (DEG mode) | `atan(1)` |
148 | | **log** | `100` → `log` → `=` | `2` | `log10(100)` |
149 | | **10ˣ** | `INV` → `2` → `log` → `=` | `100` | `10^(2)` |
150 | | **ln** | `e` → `ln` → `=` | `1` | `log(e)` |
151 | | **eˣ** | `INV` → `1` → `ln` → `=` | `2.71828...` | `exp(1)` |
152 | | **x!** | `5` → `x!` → `=` | `120` | `5!` |
153 |
154 | ### **Row 2 - Constants & Powers**
155 | | Button | Test Input | Expected Result | Expression Shown |
156 | |--------|------------|-----------------|------------------|
157 | | **π** | `π` → `=` | `3.14159265359` | `pi` |
158 | | **e** | `e` → `=` | `2.71828182846` | `e` |
159 | | **x²** | `5` → `x²` | `25` (immediate) | - |
160 | | **√x** | `9` → `√x` | `3` (immediate) | - |
161 | | **xʸ** | `2` → `xʸ` → `3` → `=` | `8` | `2^3` |
162 | | **(** | `5` → `(` → `2` → `+` → `3` → `)` → `=` | `25` | `5(2 + 3)` |
163 |
164 | ### **Row 3 - Clear & Basic Operations**
165 | | Button | Test Input | Expected Result | Notes |
166 | |--------|------------|-----------------|-------|
167 | | **AC** | Any calculation → `AC` | `0`, clears all | Resets everything |
168 | | **C** | `123` → `C` | `0` | Clears display only |
169 | | **)** | `(` → `2` → `+` → `3` → `)` | Shows in expression | Closes parentheses |
170 | | **÷** | `10` → `÷` → `2` → `=` | `5` | `10 / 2` |
171 | | **×** | `6` → `×` → `7` → `=` | `42` | `6 * 7` |
172 | | **DEL** | `123` → `DEL` | `12` | Removes last digit |
173 |
174 | ### **Row 4 - Numbers & Operations**
175 | | Button | Test Input | Expected Result | Expression Shown |
176 | |--------|------------|-----------------|------------------|
177 | | **7,8,9** | `789` | `789` | Number input |
178 | | **−** | `10` → `−` → `3` → `=` | `7` | `10 - 3` |
179 | | **%** | `50` → `%` | `0.5` (immediate) | Converts to decimal |
180 | | **1/x** | `4` → `1/x` | `0.25` (immediate) | Reciprocal |
181 |
182 | ### **Row 5 - Numbers & Operations**
183 | | Button | Test Input | Expected Result | Expression Shown |
184 | |--------|------------|-----------------|------------------|
185 | | **4,5,6** | `456` | `456` | Number input |
186 | | **+** | `5` → `+` → `3` → `=` | `8` | `5 + 3` |
187 | | **±** | `5` → `±` | `-5` (immediate) | Sign toggle |
188 | | **EXP** | `1.5` → `EXP` → `10` | `1.5e10` | Scientific notation |
189 |
190 | ### **Row 6 - Numbers & Special Functions**
191 | | Button | Test Input | Expected Result | Expression Shown |
192 | |--------|------------|-----------------|------------------|
193 | | **1,2,3** | `123` | `123` | Number input |
194 | | **=** | Any expression → `=` | Calculated result | Executes calculation |
195 | | **Ans** | Previous result → `Ans` | Last answer | Recalls last result |
196 | | **mod** | `10` → `mod` → `3` → `=` | `1` | `10 mod 3` |
197 |
198 | ### **Row 7 - Zero, Decimal & Advanced**
199 | | Button | Test Input | Expected Result | Expression Shown |
200 | |--------|------------|-----------------|------------------|
201 | | **Rand** | `Rand` | `0.xxxxx` | Random number 0-1 |
202 | | **0** | `0` or `120` | `0` or `1200` | Number input (spans 2 cols) |
203 | | **.** | `5` → `.` → `25` | `5.25` | Decimal point |
204 | | **\|x\|** | `-5` → `\|x\|` | `5` (immediate) | Absolute value |
205 |
206 | ### **Mode Toggles**
207 | | Button | Test | Expected Result |
208 | |--------|------|-----------------|
209 | | **DEG/RAD** | `π` → `sin` → `=` | DEG: `0`, RAD: `0` |
210 | | **INV** | Toggle → functions change | sin↔sin⁻¹, etc. |
211 |
212 | ## 🧪 **Complex Test Cases**
213 | 1. **Nested Functions**: `cos(sin(45))` → DEG mode → `0.99992384661`
214 | 2. **Mixed Operations**: `2^3 + sqrt(16) - log(100)` → `8 + 4 - 2 = 10`
215 | 3. **Parentheses**: `(2 + 3) * (4 - 1)` → `5 * 3 = 15`
216 | 4. **Constants**: `π * e^2` → `≈22.87`
217 | 5. **Pi with Functions**: `sin(cos(π))` → DEG mode → `0.017426180743`
218 | 6. **Scientific Notation**: `1.5e3 + 500` → `2000`
219 |
220 | ### **Additional Complex Function Tests**
221 | | Input Sequence | Expected Result | Notes |
222 | |----------------|-----------------|-------|
223 | | `45` → `sin` → `cos` → `=` | `0.99992384661` | cos(sin(45°)) in DEG mode |
224 | | `45` → `cos` → `sin` → `=` | `0.012341028215` | sin(cos(45°)) in DEG mode |
225 | | `π` → `cos` → `sin` → `=` | `0.017426180743` | sin(cos(π°)) in DEG mode |
226 | | `30` → `cos` → `sin` → `tan` → `=` | `0.0002637963853` | tan(sin(cos(30°))) in DEG mode |
227 | | `π` → `sin` → `cos` → `=` | TBD | cos(sin(π°)) in DEG mode |
228 |
229 | ## 🎯 Key Components
230 |
231 | ### Calculator Hook (`useCalculator.ts`)
232 | Central state management for all calculator operations, including:
233 | - Display state management
234 | - Memory operations
235 | - Calculation history
236 | - Mode switching
237 | - Theme toggling
238 |
239 | ### Calculator Components
240 | Each calculator mode is implemented as a separate component with specialized functionality:
241 | - Modular design for easy maintenance
242 | - Consistent UI patterns across modes
243 | - Optimized performance for complex calculations
244 |
245 | ## 🔧 Available Scripts
246 |
247 | - `npm run dev` - Start development server
248 | - `npm run build` - Build for production
249 | - `npm run preview` - Preview production build
250 | - `npm run lint` - Run ESLint
251 |
252 | ## 🌟 Features in Detail
253 |
254 | ### Memory Functions
255 | - **MC (Memory Clear)**: Clear stored memory
256 | - **MR (Memory Recall)**: Recall stored value
257 | - **M+ (Memory Add)**: Add current display to memory
258 | - **M- (Memory Subtract)**: Subtract current display from memory
259 |
260 | ### Scientific Functions
261 | - Trigonometric: sin, cos, tan (with inverse functions)
262 | - Logarithmic: log (base 10), ln (natural log)
263 | - Power operations: x², x^y, √x
264 | - Constants: π (pi), e (Euler's number)
265 | - Special functions: factorial (!), absolute value
266 |
267 | ### Graph Features
268 | - Multiple function plotting
269 | - Real-time function updates
270 | - Customizable axis ranges
271 | - Function visibility controls
272 | - Color-coded function identification
273 |
274 | ## 🤝 Contributing
275 |
276 | 1. Fork the repository
277 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
278 | 3. Commit your changes (`git commit -m 'Add amazing feature'`)
279 | 4. Push to the branch (`git push origin feature/amazing-feature`)
280 | 5. Open a Pull Request
281 |
282 | ## 📄 License
283 |
284 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
285 |
286 | ## 🙏 Acknowledgments
287 |
288 | - Developed by [seehiong](https://github.com/seehiong)
289 | - Built with [Bolt.new](https://bolt.new/) - AI-powered development platform
290 | - Mathematical calculations powered by [Math.js](https://mathjs.org/)
291 | - Charts rendered with [Chart.js](https://www.chartjs.org/)
292 | - Icons provided by [Lucide React](https://lucide.dev/)
293 | - Styling with [Tailwind CSS](https://tailwindcss.com/)
294 |
295 | ## 📞 Support
296 |
297 | If you encounter any issues or have questions, please:
298 | 1. Check the existing [issues on GitHub](https://github.com/seehiong/swift-calc/issues)
299 | 2. Create a [new issue](https://github.com/seehiong/swift-calc/issues/new) with detailed information
300 | 3. Include steps to reproduce any bugs
301 |
302 | ---
303 |
--------------------------------------------------------------------------------
/src/components/calculators/FinancialCalculator.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Calculator, DollarSign, TrendingUp, PiggyBank } from 'lucide-react';
3 |
4 | interface FinancialCalculatorProps {
5 | calculate: (expression: string) => string;
6 | }
7 |
8 | export const FinancialCalculator: React.FC = ({ calculate }) => {
9 | const [activeTab, setActiveTab] = useState<'loan' | 'compound' | 'investment'>('loan');
10 |
11 | // Loan Calculator State
12 | const [loanPrincipal, setLoanPrincipal] = useState('');
13 | const [loanRate, setLoanRate] = useState('');
14 | const [loanTerm, setLoanTerm] = useState('');
15 | const [loanPayment, setLoanPayment] = useState('');
16 |
17 | // Compound Interest State
18 | const [compoundPrincipal, setCompoundPrincipal] = useState('');
19 | const [compoundRate, setCompoundRate] = useState('');
20 | const [compoundTime, setCompoundTime] = useState('');
21 | const [compoundFrequency, setCompoundFrequency] = useState('12');
22 | const [compoundResult, setCompoundResult] = useState('');
23 |
24 | // Investment State
25 | const [investmentInitial, setInvestmentInitial] = useState('');
26 | const [investmentMonthly, setInvestmentMonthly] = useState('');
27 | const [investmentRate, setInvestmentRate] = useState('');
28 | const [investmentYears, setInvestmentYears] = useState('');
29 | const [investmentResult, setInvestmentResult] = useState('');
30 |
31 | const calculateLoanPayment = () => {
32 | const P = parseFloat(loanPrincipal);
33 | const r = parseFloat(loanRate) / 100 / 12;
34 | const n = parseFloat(loanTerm) * 12;
35 |
36 | if (P && r && n) {
37 | const payment = P * (r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);
38 | setLoanPayment(payment.toFixed(2));
39 | }
40 | };
41 |
42 | const calculateCompoundInterest = () => {
43 | const P = parseFloat(compoundPrincipal);
44 | const r = parseFloat(compoundRate) / 100;
45 | const n = parseFloat(compoundFrequency);
46 | const t = parseFloat(compoundTime);
47 |
48 | if (P && r && n && t) {
49 | const amount = P * Math.pow(1 + r / n, n * t);
50 | setCompoundResult(amount.toFixed(2));
51 | }
52 | };
53 |
54 | const calculateInvestment = () => {
55 | const P = parseFloat(investmentInitial) || 0;
56 | const PMT = parseFloat(investmentMonthly) || 0;
57 | const r = parseFloat(investmentRate) / 100 / 12;
58 | const n = parseFloat(investmentYears) * 12;
59 |
60 | if ((P || PMT) && r && n) {
61 | // Future value of initial investment
62 | const FV1 = P * Math.pow(1 + r, n);
63 |
64 | // Future value of monthly payments (annuity)
65 | const FV2 = PMT * ((Math.pow(1 + r, n) - 1) / r);
66 |
67 | const totalFV = FV1 + FV2;
68 | setInvestmentResult(totalFV.toFixed(2));
69 | }
70 | };
71 |
72 | const tabs = [
73 | { id: 'loan' as const, label: 'Loan Calculator', icon: },
74 | { id: 'compound' as const, label: 'Compound Interest', icon: },
75 | { id: 'investment' as const, label: 'Investment', icon: },
76 | ];
77 |
78 | return (
79 |
80 | {/* Tab Navigation */}
81 |
82 | {tabs.map((tab) => (
83 |
95 | ))}
96 |
97 |
98 | {/* Loan Calculator */}
99 | {activeTab === 'loan' && (
100 |
101 |
102 |
103 | Loan Payment Calculator
104 |
105 |
106 |
151 |
152 |
Monthly Payment
153 |
154 | ${loanPayment || '0.00'}
155 |
156 | {loanPayment && (
157 |
158 |
Total Interest: ${((parseFloat(loanPayment) * parseFloat(loanTerm) * 12) - parseFloat(loanPrincipal)).toFixed(2)}
159 |
Total Paid: ${(parseFloat(loanPayment) * parseFloat(loanTerm) * 12).toFixed(2)}
160 |
161 | )}
162 |
163 |
164 |
165 | )}
166 |
167 | {/* Compound Interest Calculator */}
168 | {activeTab === 'compound' && (
169 |
170 |
171 |
172 | Compound Interest Calculator
173 |
174 |
175 |
176 |
177 |
180 | setCompoundPrincipal(e.target.value)}
184 | className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
185 | placeholder="10000"
186 | />
187 |
188 |
189 |
192 | setCompoundRate(e.target.value)}
197 | className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
198 | placeholder="5"
199 | />
200 |
201 |
202 |
205 | setCompoundTime(e.target.value)}
209 | className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
210 | placeholder="10"
211 | />
212 |
213 |
214 |
217 |
228 |
229 |
235 |
236 |
237 |
Final Amount
238 |
239 | ${compoundResult || '0.00'}
240 |
241 | {compoundResult && compoundPrincipal && (
242 |
243 |
Interest Earned: ${(parseFloat(compoundResult) - parseFloat(compoundPrincipal)).toFixed(2)}
244 |
245 | )}
246 |
247 |
248 |
249 | )}
250 |
251 | {/* Investment Calculator */}
252 | {activeTab === 'investment' && (
253 |
254 |
255 |
256 | Investment Calculator
257 |
258 |
259 |
316 |
317 |
Future Value
318 |
319 | ${investmentResult || '0.00'}
320 |
321 | {investmentResult && (
322 |
323 |
Total Contributions: ${((parseFloat(investmentInitial) || 0) + (parseFloat(investmentMonthly) || 0) * parseFloat(investmentYears) * 12).toFixed(2)}
324 |
Investment Gains: ${(parseFloat(investmentResult) - ((parseFloat(investmentInitial) || 0) + (parseFloat(investmentMonthly) || 0) * parseFloat(investmentYears) * 12)).toFixed(2)}
325 |
326 | )}
327 |
328 |
329 |
330 | )}
331 |
332 | );
333 | };
--------------------------------------------------------------------------------
/src/components/calculators/ScientificCalculator.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { CalculatorButton } from '../CalculatorButton';
3 | import { Display } from '../Display';
4 |
5 | interface ScientificCalculatorProps {
6 | display: string;
7 | setDisplay: (value: string) => void;
8 | memory: number;
9 | calculate: (expression: string) => string;
10 | clearDisplay: () => void;
11 | clearAll: () => void;
12 | recallLastResult: () => void;
13 | addToHistory: (expression: string, result: string) => void;
14 | }
15 |
16 | export const ScientificCalculator: React.FC = ({
17 | display,
18 | setDisplay,
19 | memory,
20 | calculate,
21 | clearDisplay,
22 | clearAll,
23 | recallLastResult,
24 | addToHistory,
25 | }) => {
26 | const [expression, setExpression] = useState('');
27 | const [displayExpression, setDisplayExpression] = useState('');
28 | const [openParenCount, setOpenParenCount] = useState(0);
29 | const [isInverse, setIsInverse] = useState(false);
30 | const [angleMode, setAngleMode] = useState<'DEG' | 'RAD'>('DEG');
31 | const [waitingForOperand, setWaitingForOperand] = useState(false);
32 |
33 | const inputNumber = (num: string) => {
34 | if (waitingForOperand) {
35 | setDisplay(num);
36 | setWaitingForOperand(false);
37 | } else {
38 | if (num === '.' && display.includes('.')) {
39 | return; // Don't add multiple decimal points
40 | }
41 | if (display === '0' && num !== '.') {
42 | setDisplay(num);
43 | } else {
44 | setDisplay(display + num);
45 | }
46 | }
47 | };
48 |
49 | const inputOperator = (op: string) => {
50 | // Add current display to both expression and display expression
51 | const newExpression = expression + display + ' ' + op + ' ';
52 | const newDisplayExpression = displayExpression + display + ' ' + op + ' ';
53 |
54 | setExpression(newExpression);
55 | setDisplayExpression(newDisplayExpression);
56 | setWaitingForOperand(true);
57 | };
58 |
59 | const inputOpenParenthesis = () => {
60 | // If there's a number in display and we're not waiting for operand, add multiplication
61 | if (!waitingForOperand && display !== '0' && display !== '') {
62 | const newExpression = expression + display + ' * (';
63 | const newDisplayExpression = displayExpression + display + ' * (';
64 | setExpression(newExpression);
65 | setDisplayExpression(newDisplayExpression);
66 | } else {
67 | // Just add the opening parenthesis
68 | setExpression(prev => prev + '(');
69 | setDisplayExpression(prev => prev + '(');
70 | }
71 |
72 | setOpenParenCount(openParenCount + 1);
73 | setDisplay('');
74 | setWaitingForOperand(false);
75 | };
76 |
77 | const inputCloseParenthesis = () => {
78 | if (openParenCount === 0) return; // No matching open parenthesis
79 |
80 | // Add current display to expressions, then add closing parenthesis
81 | const currentValue = waitingForOperand ? '' : display;
82 | const newExpression = expression + currentValue + ')';
83 | const newDisplayExpression = displayExpression + currentValue + ')';
84 |
85 | setExpression(newExpression);
86 | setDisplayExpression(newDisplayExpression);
87 | setOpenParenCount(openParenCount - 1);
88 | setDisplay('');
89 | setWaitingForOperand(true);
90 | };
91 | const inputFunction = (func: string) => {
92 | let functionName = func;
93 | let mathJsFunction = func;
94 |
95 | if (isInverse) {
96 | switch (func) {
97 | case 'sin':
98 | functionName = 'sin⁻¹';
99 | mathJsFunction = 'asin';
100 | break;
101 | case 'cos':
102 | functionName = 'cos⁻¹';
103 | mathJsFunction = 'acos';
104 | break;
105 | case 'tan':
106 | functionName = 'tan⁻¹';
107 | mathJsFunction = 'atan';
108 | break;
109 | case 'log':
110 | functionName = '10^';
111 | mathJsFunction = '10^';
112 | break;
113 | case 'ln':
114 | functionName = 'e^';
115 | mathJsFunction = 'exp';
116 | break;
117 | }
118 | }
119 |
120 | // Always wrap the current display in the new function
121 | const newDisplay = functionName + '(' + display + ')';
122 | setDisplay(newDisplay);
123 |
124 | setWaitingForOperand(true);
125 | setIsInverse(false);
126 | };
127 |
128 | const inputFactorial = () => {
129 | setDisplay(display + '!');
130 | setWaitingForOperand(true);
131 | };
132 |
133 | const inputConstant = (constant: string) => {
134 | if (waitingForOperand || display === '0') {
135 | setDisplay(constant);
136 | setWaitingForOperand(false);
137 | } else {
138 | setDisplay(display + constant);
139 | }
140 | };
141 |
142 | const performCalculation = () => {
143 | let fullExpression = expression + display;
144 |
145 | if (fullExpression.trim() === '') {
146 | fullExpression = display;
147 | }
148 |
149 | console.log('Original expression:', fullExpression);
150 |
151 | // Replace constants first, before other transformations
152 | fullExpression = fullExpression
153 | .replace(/π/g, 'pi')
154 | .replace(/(? {
204 | const currentValue = parseFloat(display);
205 | let result: string;
206 |
207 | try {
208 | switch (func) {
209 | case 'x²':
210 | result = calculate(`(${display})^2`);
211 | break;
212 | case '√':
213 | result = calculate(`sqrt(${display})`);
214 | break;
215 | case '1/x':
216 | result = calculate(`1/(${display})`);
217 | break;
218 | case '%':
219 | result = (currentValue / 100).toString();
220 | break;
221 | case 'EXP':
222 | // Handle scientific notation (e.g., 1.5e10)
223 | if (!display.includes('e') && !waitingForOperand) {
224 | setDisplay(display + 'e');
225 | }
226 | return;
227 | case '±':
228 | result = display.startsWith('-') ? display.slice(1) : '-' + display;
229 | break;
230 | case '|x|':
231 | result = Math.abs(currentValue).toString();
232 | break;
233 | default:
234 | return;
235 | }
236 |
237 | setDisplay(result);
238 | setWaitingForOperand(true);
239 | } catch (error) {
240 | setDisplay('Error');
241 | }
242 | };
243 |
244 | const handleClear = () => {
245 | clearDisplay();
246 | setExpression('');
247 | setDisplayExpression('');
248 | setOpenParenCount(0);
249 | setWaitingForOperand(false);
250 | };
251 |
252 | const handleAllClear = () => {
253 | clearAll();
254 | setExpression('');
255 | setDisplayExpression('');
256 | setOpenParenCount(0);
257 | setWaitingForOperand(false);
258 | setIsInverse(false);
259 | };
260 |
261 | // Show the display expression with current input
262 | const fullDisplayExpression = displayExpression + (waitingForOperand ? '' : display);
263 | return (
264 |
265 |
266 |
267 | {/* Mode indicators */}
268 |
269 |
275 |
281 | {openParenCount > 0 && (
282 |
283 | {openParenCount} open paren{openParenCount > 1 ? 's' : ''}
284 |
285 | )}
286 |
287 |
288 |
289 | {/* Row 1 - Functions */}
290 | inputFunction('sin')} variant="function">
291 | {isInverse ? 'sin⁻¹' : 'sin'}
292 |
293 | inputFunction('cos')} variant="function">
294 | {isInverse ? 'cos⁻¹' : 'cos'}
295 |
296 | inputFunction('tan')} variant="function">
297 | {isInverse ? 'tan⁻¹' : 'tan'}
298 |
299 | inputFunction('log')} variant="function">
300 | {isInverse ? '10ˣ' : 'log'}
301 |
302 | inputFunction('ln')} variant="function">
303 | {isInverse ? 'eˣ' : 'ln'}
304 |
305 | x!
306 |
307 | {/* Row 2 - Powers and roots */}
308 | inputConstant('π')} variant="function">π
309 | inputConstant('e')} variant="function">e
310 | handleSpecialFunction('x²')} variant="function">x²
311 | handleSpecialFunction('√')} variant="function">√x
312 | inputOperator('^')} variant="function">xʸ
313 | (
314 |
315 | {/* Row 3 */}
316 | AC
317 | C
318 | )
319 | inputOperator('/')} variant="operator">÷
320 | inputOperator('*')} variant="operator">×
321 | {
322 | const newDisplay = display.slice(0, -1) || '0';
323 | setDisplay(newDisplay);
324 | if (newDisplay === '0') setWaitingForOperand(false);
325 | }} variant="clear">DEL
326 |
327 | {/* Row 4 */}
328 | inputNumber('7')}>7
329 | inputNumber('8')}>8
330 | inputNumber('9')}>9
331 | inputOperator('-')} variant="operator">−
332 | handleSpecialFunction('%')} variant="operator">%
333 | handleSpecialFunction('1/x')} variant="function">1/x
334 |
335 | {/* Row 5 */}
336 | inputNumber('4')}>4
337 | inputNumber('5')}>5
338 | inputNumber('6')}>6
339 | inputOperator('+')} variant="operator">+
340 | handleSpecialFunction('±')} variant="operator">±
341 | {
342 | // Handle scientific notation (e.g., 1.5e10)
343 | if (!display.includes('e') && !waitingForOperand) {
344 | setDisplay(display + 'e');
345 | }
346 | }} variant="function">EXP
347 |
348 | {/* Row 6 */}
349 | inputNumber('1')}>1
350 | inputNumber('2')}>2
351 | inputNumber('3')}>3
352 | =
353 | Ans
354 | inputOperator('mod')} variant="function">mod
355 | {
356 | setDisplay(Math.random().toString());
357 | setWaitingForOperand(true);
358 | }} variant="function">Rand
359 |
360 | {/* Row 7 */}
361 | inputNumber('0')} className="col-span-2">0
362 | {
363 | if (waitingForOperand) {
364 | setDisplay('0.');
365 | setWaitingForOperand(false);
366 | } else if (!display.includes('.')) {
367 | setDisplay(display + '.');
368 | }
369 | }}>.
370 | handleSpecialFunction('|x|')} variant="function">|x|
371 |
372 |
373 | );
374 | };
--------------------------------------------------------------------------------