diff --git a/README.md b/README.md index 1a583504..073d7abe 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,46 @@ # Instant Calc 🧮 -Instant Calc is a custom project, providing a simple and intuitive way to perform calculations and evaluate expressions in your web browser. +Real-time calculation notebook with syntax highlighting, deduction tracking, and cloud sync. -## Features ✏️ +## Features ✨ -- Natural Language Syntax: Perform calculations using familiar phrases and expressions. -- Real-Time Evaluation: Instantly see the results of your calculations as you type. -- Variable Assignment: Define and use variables to store values for repeated use. -- Expression Parsing: Evaluate complex expressions with operators, functions, and parentheses. -- Commenting: Add comments to your calculations for better organization and understanding. -- Responsive Design: Enjoy a seamless experience across different devices and screen sizes. +- **Syntax Highlighting** — headings, comments, expenses, and variables colour-coded as you type +- **Real-Time Evaluation** — results appear instantly in the output panel; no submit button needed +- **Variable Assignment** — `name = expression` or `name: expression` stores a value +- **Auto-Commenting** — set a deduction day per expense; past-due lines are commented out automatically +- **Due Next mode** — toggle to preview next month's deductions +- **Rich Text Input** — powered by Quill, with coloured formatting preserved +- **Cloud Sync** — sign in with Google to save notebooks to Firebase +- **Dark Mode** — toggle between light and dark themes +- **PWA** — installable as a progressive web app ## Usage -1. Type your calculations in the input textarea. -2. Press Enter or click outside the textarea to evaluate the expression. -3. The output will be displayed in the output textarea. -4. Store operations in a variable by starting with `name=` -5. Add comments using the `//` prefix to document your calculations. -6. Add headings using the `#` prefix for ease of labelling. -7. Make use of reserved keywords ["prev", "sum", "to", "monthlyPayDate", "in"] - -## Technologies Used - -- React.js -- Next.js -- CSS Modules -- TypeScript -- Firebase +1. Type into the left panel (rich-text editor). +2. Each line is evaluated on the fly — output appears on the right. +3. `name: value` or `name = value` creates a variable you can reuse later. +4. `//` comments out a line, `#` creates a heading. +5. Keywords: `prev` (last result), `sum` (running total), `monthlyPayDate` (days until payday). +6. Click the calendar icon 🌿 next to an expense to set its deduction day. +7. Sign in to save automatically. + +## Quick Start + +```bash +npm install +npm run dev # local dev server +npm test # vitest +npm run build # production build +npm run lint # eslint + prettier +``` + +## Tech Stack + +- **Framework:** Next.js 14 (Pages Router), React 18 +- **Language:** TypeScript +- **Editor:** react-quill (Quill rich text editor) +- **Styling:** Tailwind CSS + CSS Modules +- **State:** React hooks (useState, useCallback, useEffect) +- **Database:** Firebase Realtime Database (auth + data sync) +- **Animation:** Framer Motion +- **Testing:** Vitest diff --git a/helpers/__tests__/calculations.test.ts b/helpers/__tests__/calculations.test.ts index da5c0087..dc3c1e16 100644 --- a/helpers/__tests__/calculations.test.ts +++ b/helpers/__tests__/calculations.test.ts @@ -62,6 +62,18 @@ describe("parseVariableLine", () => { isCommented: false, }); }); + + it("returns null for empty string", () => { + expect(parseVariableLine("")).toBeNull(); + }); + + it("returns null for whitespace-only line", () => { + expect(parseVariableLine(" ")).toBeNull(); + }); + + it("returns null for tab-only line", () => { + expect(parseVariableLine("\t")).toBeNull(); + }); }); describe("getAutoCommentedLines", () => { @@ -174,6 +186,31 @@ describe("getAutoCommentedLines", () => { ); expect(result).toEqual(["rent=1000"]); }); + + it("preserves blank lines through auto-commenting", () => { + const result = getAutoCommentedLines( + ["rent: 1000", "", "food: 250"], + {}, + false, + 10, + ); + expect(result).toEqual(["rent: 1000", "", "food: 250"]); + }); + + it("handles blank lines between commented and active lines", () => { + const result = getAutoCommentedLines( + ["//rent: 1000", "", "food: 250"], + { rent: 15 }, + false, + 20, + ); + expect(result).toEqual(["//rent: 1000", "", "food: 250"]); + }); + + it("handles trailing blank line", () => { + const result = getAutoCommentedLines(["rent: 1000", ""], {}, false, 10); + expect(result).toEqual(["rent: 1000", ""]); + }); }); describe("tryGetValue", () => { diff --git a/helpers/getCurrencyConversion.ts b/helpers/getCurrencyConversion.ts index 3b6ca879..8f6619c9 100644 --- a/helpers/getCurrencyConversion.ts +++ b/helpers/getCurrencyConversion.ts @@ -3,7 +3,6 @@ import { acceptedCurrencySymbols } from "../currencies"; export const isCurrency = async (name: string) => { let withToCode: string | boolean = false; const hasIn = name.includes("in"); - console.log(hasIn); if (hasIn) { const expression = name .split("in") diff --git a/package-lock.json b/package-lock.json index c76124ee..7c65397e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "next-pwa": "^5.6.0", "react": "^18.3.0", "react-dom": "^18.3.0", + "react-quill": "^2.0.0", "tailwindcss": "^3.4.12" }, "devDependencies": { @@ -3533,6 +3534,15 @@ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", "dev": true }, + "node_modules/@types/quill": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz", + "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==", + "license": "MIT", + "dependencies": { + "parchment": "^1.1.2" + } + }, "node_modules/@types/react": { "version": "18.3.5", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", @@ -4648,6 +4658,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4913,6 +4952,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5059,6 +5107,26 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "license": "MIT", + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5260,6 +5328,20 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -5377,12 +5459,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -5423,6 +5503,18 @@ "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", @@ -6044,6 +6136,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -6100,6 +6198,12 @@ "node": ">=12.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6109,7 +6213,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", - "dev": true, "license": "Apache-2.0" }, "node_modules/fast-glob": { @@ -6496,15 +6599,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -6518,6 +6627,19 @@ "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -6645,11 +6767,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6705,9 +6828,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -6730,9 +6854,10 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -6837,6 +6962,22 @@ "node": ">= 0.4" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -8211,6 +8352,15 @@ "semver": "bin/semver.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mathjs": { "version": "15.2.0", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-15.2.0.tgz", @@ -8675,6 +8825,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -8874,6 +9040,12 @@ "node": ">=6" } }, + "node_modules/parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==", + "license": "BSD-3-Clause" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9411,6 +9583,34 @@ } ] }, + "node_modules/quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "license": "BSD-3-Clause", + "dependencies": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "node_modules/quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "license": "MIT", + "dependencies": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -9450,6 +9650,21 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-quill": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz", + "integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==", + "license": "MIT", + "dependencies": { + "@types/quill": "^1.3.10", + "lodash": "^4.17.4", + "quill": "^1.3.7" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index 89a3e391..3ec23b7e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "next-pwa": "^5.6.0", "react": "^18.3.0", "react-dom": "^18.3.0", + "react-quill": "^2.0.0", "tailwindcss": "^3.4.12" }, "devDependencies": { diff --git a/pages/about.tsx b/pages/about.tsx index 8bebf9b9..38f99cad 100644 --- a/pages/about.tsx +++ b/pages/about.tsx @@ -33,7 +33,7 @@ const useTypewriter = ( }, speed); return () => clearTimeout(timer); } - }, [text, index, speed, isDeleting, displayedText]); + }, [text, index, speed, isDeleting, displayedText, setIndex]); return displayedText; }; @@ -148,14 +148,9 @@ export default function About() {

About Instant Calc 🧮

- Instant Calc is a cutting-edge calculation tool designed to revolutionize the way you - work with numbers. Our platform combines lightning-fast processing with intuitive - design, making complex computations a breeze for users of all levels. -

-

- Whether you're a student tackling advanced mathematics, a professional dealing with - intricate financial models, or simply someone who loves efficient problem-solving, - Instant Calc is your ultimate companion for all calculation needs. + Instant Calc is a real-time calculation notebook. Type expenses, set variables, write + expressions — and see results instantly on the right. Every line is evaluated as you + type, with support for comments, headings, and deduction tracking.

Key Features ✨

Usage Guide 📘

    -
  1. Type your calculations in the input textarea.
  2. -
  3. Press Enter or click outside the textarea to evaluate the expression.
  4. -
  5. The output will be displayed in the output textarea.
  6. -
  7. Store operations in a variable by starting with name=
  8. -
  9. Add comments using the // prefix to document your calculations.
  10. -
  11. Add headings using the # prefix for ease of labelling.
  12. +
  13. Type your calculations in the rich-text editor (left panel).
  14. +
  15. Each line is evaluated automatically — output appears on the right.
  16. +
  17. + Use name = value or{" "} + name: value to store a variable. +
  18. +
  19. + Use {"//"} to comment out a line,{" "} + {"#"} for headings. +
  20. +
  21. + Reserved keywords: prev (last result),{" "} + sum (running total),{" "} + monthlyPayDate (pay-day countdown). +
  22. +
  23. + Click the calendar icon next to an expense to set its deduction day — it auto-comments + when past due. +
  24. - Make use of reserved keywords ["prev", "sum", "to", - "monthlyPayDate", "in"] + Toggle Due Next to uncomment all deduction lines for next-month + planning.
  25. +
  26. Sign in to save your notebook automatically to the cloud.

Instant Calc is designed to be intuitive and user-friendly. Start exploring its @@ -264,14 +276,14 @@ export default function About() { we're planning to implement:

    -
  • 📊 Data Visualization: Show charts and graphs for your calculations.
  • -
  • 📑 Multiple Tabs: Add and save more tabs for different calculation sessions.
  • +
  • 📊 Charts & Visualisations for expense breakdowns.
  • +
  • 📑 Multiple Tabs for different notebooks.
  • +
  • 📤 Export to CSV / PDF.
  • +
  • 📱 Progressive Web App — install on your phone.
  • +
  • 🌐 Real-time multi-user collaboration.
  • - 📤 Export Functionality: Export your calculations and results in various formats. + 🧮 Currency conversion (100 usd in gbp).
  • -
  • 📅 Pay Date Setting: Set a date for pay dates and track expenses that are due.
  • -
  • 💾 Auto-Save: Automatically save your work to prevent data loss.
  • -
  • 🔄 Sync Across Devices: Access your calculations from any device.

Stay tuned for these exciting updates that will make Instant Calc even more powerful and diff --git a/pages/index.tsx b/pages/index.tsx index dc1cf432..b1742f57 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -15,7 +15,11 @@ import { get, ref, set } from "firebase/database"; import { motion } from "framer-motion"; import { Calendar, Edit2, Moon, Save, Sun, Trash2 } from "lucide-react"; import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react"; +import dynamic from "next/dynamic"; import "tailwindcss/tailwind.css"; + +const ReactQuill = dynamic(() => import("react-quill"), { ssr: false }); +import "react-quill/dist/quill.snow.css"; import { evaluateExpression } from "../helpers/calculate"; import { getAutoCommentedLines, computeSummary, parseVariableLine } from "../helpers/calculations"; @@ -46,6 +50,9 @@ export default function Index() { const inputRef = useRef(null); const outputRef = useRef(null); + const inputContainerRef = useRef(null); + const quillEditorRef = useRef(null); + const quillScrollRef = useRef(null); const variablesRef = useRef<{ [name: string]: number }>({}); const originalValuesRef = useRef<{ [name: string]: number }>({}); const [scrollTop, setScrollTop] = useState(0); @@ -72,15 +79,11 @@ export default function Index() { }, [calculations]); useEffect(() => { - if (inputRef.current) { - inputRef.current.style.height = "auto"; - inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; - } if (outputRef.current) { outputRef.current.style.height = "auto"; outputRef.current.style.height = `${outputRef.current.scrollHeight}px`; } - }, [input, output]); + }, [output]); useEffect(() => { handleInput(); @@ -92,6 +95,102 @@ export default function Index() { } }, [isEditingName]); + const highlightSyntax = useCallback((editor: any) => { + if (!editor) return; + const text = editor.getText() || ""; + const lines = text.split("\n"); + let offset = 0; + for (const line of lines) { + const trimmed = line.trim(); + const length = line.length; + if (trimmed.startsWith("#")) { + editor.formatText(offset, length, { color: "#16a34a", bold: true }); + } else if (trimmed.startsWith("//")) { + editor.formatText(offset, length, { color: "#9ca3af", italic: true }); + } else if (/^monthlypaydate\b/i.test(trimmed)) { + const eqIdx = trimmed.indexOf("="); + const keywordLen = eqIdx >= 0 ? eqIdx : trimmed.length; + editor.formatText(offset, keywordLen, { color: "#3b82f6" }); + } else { + const colonIdx = trimmed.indexOf(":"); + const eqIdx = trimmed.indexOf("="); + const sepIdx = eqIdx >= 0 && (colonIdx < 0 || eqIdx < colonIdx) ? eqIdx : colonIdx; + if (sepIdx > 0) { + const nameLen = trimmed.slice(0, sepIdx).length; + const color = trimmed[sepIdx] === ":" ? "#d97706" : "#06b6d4"; + editor.formatText(offset, nameLen, { color, bold: false }); + } + } + offset += length + 1; + } + }, []); + + const handleInputScroll = useCallback(() => { + const container = quillScrollRef.current; + if (!container) return; + setScrollTop(container.scrollTop); + if (outputRef.current) { + outputRef.current.scrollTop = container.scrollTop; + } + }, []); + + const attachQuillScroll = useCallback(() => { + quillScrollRef.current?.removeEventListener("scroll", handleInputScroll); + const container = inputContainerRef.current?.querySelector(".ql-container"); + if (container) { + quillScrollRef.current = container; + container.addEventListener("scroll", handleInputScroll); + } + }, [handleInputScroll]); + + const getQuillEditor = useCallback(() => { + if (quillEditorRef.current) return quillEditorRef.current; + const root = inputContainerRef.current; + if (!root) return null; + const editorEl = root.querySelector(".ql-editor"); + const container = editorEl?.closest(".ql-container") as any; + if (container?.__quill) { + quillEditorRef.current = container.__quill; + } + return quillEditorRef.current; + }, []); + + const handleQuillChange = useCallback( + (_value: string, _delta: any, source: string, _editor: any) => { + if (source !== "user") return; + const editor = getQuillEditor(); + if (!editor) return; + attachQuillScroll(); + const text = (editor.getText() || "").replace(/\n$/, ""); + if (text !== input) { + setInput(text); + } + requestAnimationFrame(() => highlightSyntax(editor)); + }, + [input, highlightSyntax, attachQuillScroll, getQuillEditor], + ); + + useEffect(() => { + const editor = getQuillEditor(); + if (!editor) return; + const currentText = (editor.getText() || "").replace(/\n$/, ""); + if (currentText !== input) { + editor.setText(input || ""); + } + requestAnimationFrame(() => highlightSyntax(editor)); + }, [input, highlightSyntax, getQuillEditor]); + + useEffect(() => { + attachQuillScroll(); + const container = quillScrollRef.current; + return () => { + container?.removeEventListener("scroll", handleInputScroll); + }; + }, [attachQuillScroll, handleInputScroll]); + + const quillModules = { toolbar: false }; + const quillFormats = ["bold", "italic", "color"]; + useEffect(() => { localStorage.setItem("deductionDates", JSON.stringify(deductionDates)); }, [deductionDates]); @@ -252,17 +351,11 @@ export default function Index() { } }, [input, deductionDates, showNextMonth]); - const handleInputScroll = (event: React.UIEvent) => { - setScrollTop(event.currentTarget.scrollTop); - if (inputRef.current && outputRef.current) { - outputRef.current.scrollTop = event.currentTarget.scrollTop; - } - }; - const handleOutputScroll = (event: React.UIEvent) => { setScrollTop(event.currentTarget.scrollTop); - if (inputRef.current && outputRef.current) { - inputRef.current.scrollTop = event.currentTarget.scrollTop; + const container = quillScrollRef.current; + if (container) { + container.scrollTop = event.currentTarget.scrollTop; } }; @@ -444,29 +537,21 @@ export default function Index() { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.5 }} > -

- -
- -
+ {submitted ? ( +
+

Thank you!

+

+ We review every request and keep the community posted. +

- + ) : ( +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ )}
diff --git a/styles/Dark.css b/styles/Dark.css index 16e0c908..9d51cd00 100644 --- a/styles/Dark.css +++ b/styles/Dark.css @@ -85,3 +85,39 @@ -5em 0 0 -2.7em #fff, -3.5em -3.5em 0 -3em #fff; } + +.quill-editor .ql-toolbar { + display: none; +} +.quill-editor .ql-container { + font-family: inherit; + font-size: inherit; + border: none; + background: transparent; + overflow-y: auto; + overflow-x: hidden; +} +.quill-editor .ql-editor { + padding: 8px 16px 8px 16px; + line-height: 48px; + min-height: calc(100vh - 400px); + height: auto; + outline: none; + overflow: hidden; + background-image: repeating-linear-gradient(transparent, transparent 47px, #999 47px, #999 48px, transparent 48px, transparent 95px, #999 95px, #999 96px); +} +.quill-editor .ql-editor p { + margin: 0; + line-height: inherit; +} +.dark .quill-editor .ql-editor { + color: #fff; +} +.light .quill-editor .ql-editor { + color: #403c3d; +} +.quill-editor .ql-editor.ql-blank::before { + color: #9ca3af; + font-style: normal; + left: 2px; +}