diff --git a/app/(app)/calculate/page.tsx b/app/(app)/calculate/page.tsx new file mode 100644 index 0000000..b1fa9f1 --- /dev/null +++ b/app/(app)/calculate/page.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from 'next'; +import Calculator from '@/components/calculate/Calculator'; +import { AppPage } from '@/components/layout/AppPage'; +import { getToolByHref } from '@/lib/tools'; + +const tool = getToolByHref('/calculate')!; + +export const metadata: Metadata = { title: tool.title, description: tool.summary }; + +export default function CalculatePage() { + return ( + + + + ); +} diff --git a/components/AppIcons.tsx b/components/AppIcons.tsx index c88ba98..2fa5b90 100644 --- a/components/AppIcons.tsx +++ b/components/AppIcons.tsx @@ -66,3 +66,15 @@ export const QRCodeIcon = (props: React.SVGProps) => ( ); + +export const CalculateIcon = (props: React.SVGProps) => ( + + {/* Y-axis */} + + {/* X-axis */} + + {/* Smooth curve resembling sin/cos */} + + +); diff --git a/components/calculate/Calculator.tsx b/components/calculate/Calculator.tsx new file mode 100644 index 0000000..b57b6fd --- /dev/null +++ b/components/calculate/Calculator.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { useState } from 'react'; +import { cn } from '@/lib/utils'; +import { ExpressionPanel } from './ExpressionPanel'; +import { GraphPanel } from './GraphPanel'; + +type Tab = 'calc' | 'graph'; + +export default function Calculator() { + const [tab, setTab] = useState('calc'); + + return ( +
+ + {/* Mobile tab switcher — hidden on lg+ */} +
+ {(['calc', 'graph'] as Tab[]).map((t) => ( + + ))} +
+ + {/* Main layout — side-by-side on lg, tabbed on mobile */} +
+ {/* Expression panel */} +
+ +
+ + {/* Graph panel */} +
+ +
+
+ +
+ ); +} diff --git a/components/calculate/ExpressionPanel.tsx b/components/calculate/ExpressionPanel.tsx new file mode 100644 index 0000000..e00d21f --- /dev/null +++ b/components/calculate/ExpressionPanel.tsx @@ -0,0 +1,328 @@ +'use client'; + +import { useState, useRef, useCallback, useEffect } from 'react'; +import { Plus, Trash2, X, ChevronDown, ChevronUp } from 'lucide-react'; +import { useCalculateStore } from '@/lib/calculate/store'; +import { evaluateExpression } from '@/lib/calculate/math-engine'; +import { cn } from '@/lib/utils'; + +const QUICK_KEYS = [ + // Constants + { label: 'π', insert: 'pi', group: 'const' }, + { label: 'e', insert: 'e', group: 'const' }, + { label: 'φ', insert: '(1+sqrt(5))/2', group: 'const' }, + { label: '∞', insert: 'Infinity', group: 'const' }, + { label: 'i', insert: 'i', group: 'const' }, + // Ops + { label: '^', insert: '^', group: 'op' }, + { label: '(', insert: '(', group: 'op' }, + { label: ')', insert: ')', group: 'op' }, + { label: '%', insert: ' % ', group: 'op' }, + { label: 'mod', insert: ' mod ', group: 'op' }, + // Functions + { label: '√', insert: 'sqrt(', group: 'fn' }, + { label: '∛', insert: 'cbrt(', group: 'fn' }, + { label: '|x|', insert: 'abs(', group: 'fn' }, + { label: 'n!', insert: '!', group: 'fn' }, + { label: 'sin', insert: 'sin(', group: 'trig' }, + { label: 'cos', insert: 'cos(', group: 'trig' }, + { label: 'tan', insert: 'tan(', group: 'trig' }, + { label: 'asin', insert: 'asin(', group: 'trig' }, + { label: 'acos', insert: 'acos(', group: 'trig' }, + { label: 'atan', insert: 'atan(', group: 'trig' }, + { label: 'sinh', insert: 'sinh(', group: 'trig' }, + { label: 'cosh', insert: 'cosh(', group: 'trig' }, + { label: 'log', insert: 'log10(', group: 'log' }, + { label: 'ln', insert: 'log(', group: 'log' }, + { label: 'log₂', insert: 'log2(', group: 'log' }, + { label: 'exp', insert: 'exp(', group: 'log' }, + { label: 'floor', insert: 'floor(', group: 'round' }, + { label: 'ceil', insert: 'ceil(', group: 'round' }, + { label: 'round', insert: 'round(', group: 'round' }, + { label: 'gcd', insert: 'gcd(', group: 'misc' }, + { label: 'lcm', insert: 'lcm(', group: 'misc' }, + { label: 'nCr', insert: 'combinations(', group: 'misc' }, + { label: 'nPr', insert: 'permutations(', group: 'misc' }, +] as const; + +export function ExpressionPanel() { + const { + expression, setExpression, + history, addToHistory, clearHistory, + variables, setVariable, removeVariable, + } = useCalculateStore(); + + const [liveResult, setLiveResult] = useState<{ result: string; error: boolean } | null>(null); + const [newVarName, setNewVarName] = useState(''); + const [newVarValue, setNewVarValue] = useState(''); + const [showAddVar, setShowAddVar] = useState(false); + const [showAllKeys, setShowAllKeys] = useState(false); + const textareaRef = useRef(null); + + // Real-time evaluation + useEffect(() => { + if (!expression.trim()) { setLiveResult(null); return; } + const r = evaluateExpression(expression, variables); + setLiveResult(r.result ? { result: r.result, error: r.error } : null); + }, [expression, variables]); + + const handleSubmit = useCallback(() => { + if (!expression.trim()) return; + const r = evaluateExpression(expression, variables); + if (!r.result) return; + addToHistory({ expression: expression.trim(), result: r.result, error: r.error }); + if (!r.error) { + if (r.assignedName && r.assignedValue) { + setVariable(r.assignedName, r.assignedValue); + } + setExpression(''); + } + }, [expression, variables, addToHistory, setExpression, setVariable]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }, [handleSubmit]); + + const insertAtCursor = useCallback((text: string) => { + const ta = textareaRef.current; + if (!ta) { setExpression(expression + text); return; } + const start = ta.selectionStart; + const end = ta.selectionEnd; + const next = expression.slice(0, start) + text + expression.slice(end); + setExpression(next); + requestAnimationFrame(() => { + ta.focus(); + const pos = start + text.length; + ta.selectionStart = ta.selectionEnd = pos; + }); + }, [expression, setExpression]); + + const addVar = useCallback(() => { + if (!newVarName.trim() || !newVarValue.trim()) return; + setVariable(newVarName.trim(), newVarValue.trim()); + setNewVarName(''); setNewVarValue(''); setShowAddVar(false); + }, [newVarName, newVarValue, setVariable]); + + const visibleKeys = showAllKeys ? QUICK_KEYS : QUICK_KEYS.slice(0, 16); + + return ( +
+ + {/* ── Expression input ──────────────────────────────────── */} +
+
+ + Expression + + + Enter to evaluate · Shift+Enter for newline + +
+ +