inital Jack
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
.git/
|
||||
*.sh
|
||||
*.bat
|
||||
README.md
|
||||
compose.yaml
|
||||
Dockerfile
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# Use a lightweight Nginx image
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy the static files to the Nginx html directory
|
||||
COPY . /usr/share/nginx/html/
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Start Nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,59 @@
|
||||
# Calculator
|
||||
|
||||
A simple, scientific calculator that runs in your browser.
|
||||
|
||||
## Dependencies
|
||||
|
||||
This project can be run in a few different ways, and dependencies vary based on the method you choose.
|
||||
|
||||
- **Browser Only**: No dependencies, but the calculator will not be served from a webserver.
|
||||
- **Python**: To serve the `index.html` file, you'll need Python installed.
|
||||
- **Docker**: To run the project in a container, you'll need Docker installed.
|
||||
|
||||
## How to Run
|
||||
|
||||
### Windows
|
||||
|
||||
- **With Python**: Run `start-server.bat`. This will start a Python web server and open the calculator in your default browser.
|
||||
- **With Docker**: Run `run-docker.bat`. This will use Docker Compose to build and run the container.
|
||||
|
||||
### Linux / macOS
|
||||
|
||||
- **With Python**: Run `./start-server.sh`. You may need to make it executable first with `chmod +x start-server.sh`. This will start a Python web server and open the calculator in your default browser.
|
||||
- **With Docker**: Run `./run-docker.sh`. You may need to make it executable first with `chmod +x run-docker.sh`. This will use Docker Compose to build and run the container.
|
||||
|
||||
## Server Setup
|
||||
|
||||
To deploy the calculator on a server, you only need Docker and Docker Compose installed. You do not need to clone this repository.
|
||||
|
||||
1. **Install Docker and Docker Compose**: Follow the official installation instructions for your server's operating system.
|
||||
|
||||
2. **Create a `compose.yaml` file**: On your server, create a new file named `compose.yaml` and add the following content:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
image: ghcr.io/jones013/jack_calculator:latest
|
||||
ports:
|
||||
- "8888:80"
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
3. **Start the container**: In the same directory where you created the `compose.yaml` file, run the following command:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The `-d` flag runs the container in detached mode. The `restart: unless-stopped` policy ensures that the container will automatically restart if the server is rebooted.
|
||||
|
||||
The calculator will then be available at `http://<your-server-ip>:8888`.
|
||||
|
||||
**Note on Firewalls**: Ensure that your server's firewall is configured to allow incoming traffic on port 8888.
|
||||
|
||||
---
|
||||
|
||||
## Branches
|
||||
|
||||
Use `**main`** or `**user**` for this layout. Maintainer tooling and full docs live on the `**developer**` branch.
|
||||
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
image: calculator:latest
|
||||
ports:
|
||||
- "8888:80"
|
||||
restart: unless-stopped
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
+671
@@ -0,0 +1,671 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Scientific Calculator</title>
|
||||
<link rel="icon" type="image/png" href="images/gigachad.png" />
|
||||
<link rel="apple-touch-icon" href="images/gigachad.png" />
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1419;
|
||||
--surface: #1a2332;
|
||||
--key: #243044;
|
||||
--key-hover: #2d3d54;
|
||||
--accent: #3d8bfd;
|
||||
--accent-hover: #5a9dff;
|
||||
--text: #e8eef7;
|
||||
--muted: #8a9bb0;
|
||||
--danger: #e85d5d;
|
||||
--sci: #1e2a3d;
|
||||
--sci-hover: #263548;
|
||||
--radius: 10px;
|
||||
font-family: "Segoe UI", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(ellipse 120% 80% at 50% -20%, #1e3a5f 0%, var(--bg) 55%);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.calc {
|
||||
width: min(100% - 2rem, 680px);
|
||||
padding: 1.25rem;
|
||||
background: var(--surface);
|
||||
border-radius: calc(var(--radius) + 4px);
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.hero-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
margin: 0 auto 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--accent);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.hero-img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 250px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.calc-panel {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.65rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.display-wrap {
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.65rem 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
min-height: 3.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.expression {
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
min-height: 1.15em;
|
||||
word-break: break-all;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.result {
|
||||
font-size: 1.5rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mode-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
font: inherit;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
background: var(--sci);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.mode-btn:hover {
|
||||
background: var(--sci-hover);
|
||||
}
|
||||
|
||||
.keys {
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.keys.sci {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.keys.main {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
font-size: 1rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
padding: 0.72rem 0.2rem;
|
||||
cursor: pointer;
|
||||
background: var(--key);
|
||||
color: var(--text);
|
||||
transition: background 0.12s ease, transform 0.08s ease;
|
||||
}
|
||||
|
||||
button.sci {
|
||||
font-size: 0.78rem;
|
||||
padding: 0.55rem 0.15rem;
|
||||
background: var(--sci);
|
||||
}
|
||||
|
||||
button.sci:hover {
|
||||
background: var(--sci-hover);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--key-hover);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
button.op {
|
||||
background: #2a3548;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
button.op:hover {
|
||||
background: #33425a;
|
||||
}
|
||||
|
||||
button.equals {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
button.equals:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
button.zero {
|
||||
grid-column: span 2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="calc" role="application" aria-label="Scientific calculator">
|
||||
<div class="hero-wrap">
|
||||
<img
|
||||
class="hero-img"
|
||||
src="images/gigachad.png"
|
||||
alt="GigaChad meme portrait"
|
||||
/>
|
||||
</div>
|
||||
<div class="calc-panel">
|
||||
<h1>Scientific calculator</h1>
|
||||
<div class="mode-row">
|
||||
<button type="button" class="mode-btn" id="degBtn" title="Toggle angle units for sin/cos/tan and inverse trig">DEG</button>
|
||||
</div>
|
||||
<div class="display-wrap">
|
||||
<div class="expression" id="expr" aria-live="polite"></div>
|
||||
<div class="result" id="out">0</div>
|
||||
</div>
|
||||
<div class="keys sci">
|
||||
<button type="button" class="sci danger" data-action="clear">C</button>
|
||||
<button type="button" class="sci" data-action="back">⌫</button>
|
||||
<button type="button" class="sci op" data-append="(">(</button>
|
||||
<button type="button" class="sci op" data-append=")">)</button>
|
||||
<button type="button" class="sci op" data-action="ans">ANS</button>
|
||||
<button type="button" class="sci" data-append="sin(">sin</button>
|
||||
<button type="button" class="sci" data-append="cos(">cos</button>
|
||||
<button type="button" class="sci" data-append="tan(">tan</button>
|
||||
<button type="button" class="sci" data-append="asin(">asin</button>
|
||||
<button type="button" class="sci" data-append="acos(">acos</button>
|
||||
<button type="button" class="sci" data-append="atan(">atan</button>
|
||||
<button type="button" class="sci" data-append="log(">log</button>
|
||||
<button type="button" class="sci" data-append="ln(">ln</button>
|
||||
<button type="button" class="sci" data-append="sqrt(">√</button>
|
||||
<button type="button" class="sci op" data-append="^">x^y</button>
|
||||
<button type="button" class="sci op" data-append="^2">x²</button>
|
||||
<button type="button" class="sci op" data-append="^(-1)">1/x</button>
|
||||
<button type="button" class="sci op" data-append="pi">π</button>
|
||||
<button type="button" class="sci op" data-append="e">e</button>
|
||||
<button type="button" class="sci op" data-append="!">!</button>
|
||||
</div>
|
||||
<div class="keys main">
|
||||
<button type="button" class="op" data-append="/">÷</button>
|
||||
<button type="button" class="op" data-append="*">×</button>
|
||||
<button type="button" class="op" data-append="-">−</button>
|
||||
<button type="button" class="op" data-append="+">+</button>
|
||||
<button type="button" data-digit="7">7</button>
|
||||
<button type="button" data-digit="8">8</button>
|
||||
<button type="button" data-digit="9">9</button>
|
||||
<button type="button" class="op equals" data-action="equals">=</button>
|
||||
<button type="button" data-digit="4">4</button>
|
||||
<button type="button" data-digit="5">5</button>
|
||||
<button type="button" data-digit="6">6</button>
|
||||
<button type="button" data-digit="1">1</button>
|
||||
<button type="button" data-digit="2">2</button>
|
||||
<button type="button" data-digit="3">3</button>
|
||||
<button type="button" class="zero" data-digit="0">0</button>
|
||||
<button type="button" data-action="dot">.</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const exprEl = document.getElementById("expr");
|
||||
const outEl = document.getElementById("out");
|
||||
const degBtn = document.getElementById("degBtn");
|
||||
|
||||
let exprStr = "";
|
||||
let lastResult = 0;
|
||||
let degMode = true;
|
||||
|
||||
function formatDisplay(n) {
|
||||
if (!Number.isFinite(n)) return "Error";
|
||||
const s = String(n);
|
||||
if (s.length > 16) return n.toPrecision(12).replace(/\.?0+e/, "e");
|
||||
return s;
|
||||
}
|
||||
|
||||
function prettyExpr(s) {
|
||||
if (!s) return "";
|
||||
return s
|
||||
.replace(/\*/g, "×")
|
||||
.replace(/\//g, "÷")
|
||||
.replace(/pi/g, "π");
|
||||
}
|
||||
|
||||
function updateView() {
|
||||
exprEl.textContent = prettyExpr(exprStr);
|
||||
outEl.textContent = formatDisplay(lastResult);
|
||||
}
|
||||
|
||||
function factorial(n) {
|
||||
if (!Number.isFinite(n)) throw new Error("domain");
|
||||
if (n < 0 || Math.abs(n - Math.round(n)) > 1e-12) throw new Error("domain");
|
||||
const k = Math.round(n);
|
||||
if (k > 170) throw new Error("overflow");
|
||||
let r = 1;
|
||||
for (let i = 2; i <= k; i++) r *= i;
|
||||
return r;
|
||||
}
|
||||
|
||||
function applyFunc(name, x) {
|
||||
const rad = (d) => (d * Math.PI) / 180;
|
||||
const deg = (r) => (r * 180) / Math.PI;
|
||||
switch (name) {
|
||||
case "sin":
|
||||
return degMode ? Math.sin(rad(x)) : Math.sin(x);
|
||||
case "cos":
|
||||
return degMode ? Math.cos(rad(x)) : Math.cos(x);
|
||||
case "tan":
|
||||
return degMode ? Math.tan(rad(x)) : Math.tan(x);
|
||||
case "asin":
|
||||
if (degMode) return deg(Math.asin(x));
|
||||
return Math.asin(x);
|
||||
case "acos":
|
||||
if (degMode) return deg(Math.acos(x));
|
||||
return Math.acos(x);
|
||||
case "atan":
|
||||
if (degMode) return deg(Math.atan(x));
|
||||
return Math.atan(x);
|
||||
case "log":
|
||||
return Math.log10(x);
|
||||
case "ln":
|
||||
return Math.log(x);
|
||||
case "sqrt":
|
||||
return Math.sqrt(x);
|
||||
case "abs":
|
||||
return Math.abs(x);
|
||||
default:
|
||||
throw new Error("fn");
|
||||
}
|
||||
}
|
||||
|
||||
function tokenize(str) {
|
||||
const tokens = [];
|
||||
let i = 0;
|
||||
const peek = () => str[i];
|
||||
const readNum = () => {
|
||||
let start = i;
|
||||
if (peek() === ".") {
|
||||
i++;
|
||||
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
||||
} else {
|
||||
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
||||
if (peek() === ".") {
|
||||
i++;
|
||||
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
||||
}
|
||||
}
|
||||
const raw = str.slice(start, i);
|
||||
if (raw === "." || raw === "") throw new Error("num");
|
||||
const v = parseFloat(raw);
|
||||
if (!Number.isFinite(v)) throw new Error("num");
|
||||
return { type: "number", value: v };
|
||||
};
|
||||
|
||||
while (i < str.length) {
|
||||
const c = peek();
|
||||
if (/\s/.test(c)) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (/[0-9.]/.test(c)) {
|
||||
tokens.push(readNum());
|
||||
continue;
|
||||
}
|
||||
if (/[a-zA-Z]/.test(c)) {
|
||||
let j = i;
|
||||
while (j < str.length && /[a-zA-Z]/.test(str[j])) j++;
|
||||
const id = str.slice(i, j);
|
||||
i = j;
|
||||
if (id === "pi") tokens.push({ type: "const", name: "pi" });
|
||||
else if (id === "e") tokens.push({ type: "const", name: "e" });
|
||||
else if (
|
||||
["sin", "cos", "tan", "asin", "acos", "atan", "log", "ln", "sqrt", "abs"].includes(id)
|
||||
) {
|
||||
tokens.push({ type: "func", name: id });
|
||||
} else throw new Error("id");
|
||||
continue;
|
||||
}
|
||||
if (c === "(") {
|
||||
tokens.push({ type: "lparen" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === ")") {
|
||||
tokens.push({ type: "rparen" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === "!") {
|
||||
tokens.push({ type: "fact" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if ("+-*/^".includes(c)) {
|
||||
tokens.push({ type: "op", v: c });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
throw new Error("tok");
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function insertImplicitMul(tokens) {
|
||||
const out = [];
|
||||
const needs = (a, b) => {
|
||||
if (!a || !b) return false;
|
||||
const end = ["number", "rparen", "fact"];
|
||||
const start = ["number", "lparen", "func", "const"];
|
||||
return end.includes(a.type) && start.includes(b.type);
|
||||
};
|
||||
for (let k = 0; k < tokens.length; k++) {
|
||||
const t = tokens[k];
|
||||
if (k > 0 && needs(out[out.length - 1], t)) {
|
||||
out.push({ type: "op", v: "*" });
|
||||
}
|
||||
out.push(t);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseAndEval(tokens) {
|
||||
let p = 0;
|
||||
const peek = () => tokens[p];
|
||||
const take = () => tokens[p++];
|
||||
|
||||
function parseAdditive() {
|
||||
let left = parseMultiplicative();
|
||||
while (peek()?.type === "op" && (peek().v === "+" || peek().v === "-")) {
|
||||
const op = take().v;
|
||||
const right = parseMultiplicative();
|
||||
left = op === "+" ? left + right : left - right;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
function parseMultiplicative() {
|
||||
let left = parsePower();
|
||||
while (peek()?.type === "op" && (peek().v === "*" || peek().v === "/")) {
|
||||
const op = take().v;
|
||||
const right = parsePower();
|
||||
left = op === "*" ? left * right : left / right;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
function parsePower() {
|
||||
let left = parseUnary();
|
||||
if (peek()?.type === "op" && peek().v === "^") {
|
||||
take();
|
||||
const right = parsePower();
|
||||
left = Math.pow(left, right);
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
function parseUnary() {
|
||||
if (peek()?.type === "op" && peek().v === "-") {
|
||||
take();
|
||||
return -parseUnary();
|
||||
}
|
||||
if (peek()?.type === "op" && peek().v === "+") {
|
||||
take();
|
||||
return parseUnary();
|
||||
}
|
||||
return parsePostfix();
|
||||
}
|
||||
|
||||
function parsePostfix() {
|
||||
let val = parsePrimary();
|
||||
while (peek()?.type === "fact") {
|
||||
take();
|
||||
val = factorial(val);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
function parsePrimary() {
|
||||
const t = peek();
|
||||
if (!t) throw new Error("exp");
|
||||
if (t.type === "number") {
|
||||
take();
|
||||
return t.value;
|
||||
}
|
||||
if (t.type === "const") {
|
||||
take();
|
||||
return t.name === "pi" ? Math.PI : Math.E;
|
||||
}
|
||||
if (t.type === "func") {
|
||||
const name = take().name;
|
||||
if (peek()?.type !== "lparen") throw new Error("(");
|
||||
take();
|
||||
const inner = parseAdditive();
|
||||
if (peek()?.type !== "rparen") throw new Error(")");
|
||||
take();
|
||||
return applyFunc(name, inner);
|
||||
}
|
||||
if (t.type === "lparen") {
|
||||
take();
|
||||
const inner = parseAdditive();
|
||||
if (peek()?.type !== "rparen") throw new Error(")");
|
||||
take();
|
||||
return inner;
|
||||
}
|
||||
throw new Error("prim");
|
||||
}
|
||||
|
||||
const result = parseAdditive();
|
||||
if (peek()) throw new Error("trail");
|
||||
if (!Number.isFinite(result)) throw new Error("nan");
|
||||
return result;
|
||||
}
|
||||
|
||||
function evaluate(raw) {
|
||||
const sub = String(raw).trim();
|
||||
if (!sub) throw new Error("empty");
|
||||
let s = sub.replace(/\bANS\b/g, "(" + String(lastResult) + ")");
|
||||
const tokens = insertImplicitMul(tokenize(s));
|
||||
return parseAndEval(tokens);
|
||||
}
|
||||
|
||||
function lastChar() {
|
||||
return exprStr.slice(-1);
|
||||
}
|
||||
|
||||
function shouldInsertMul(next) {
|
||||
if (!exprStr) return false;
|
||||
const a = lastChar();
|
||||
const b = next[0];
|
||||
if (/[0-9]/.test(a) && /[0-9]/.test(b)) return false;
|
||||
if (a === "^") return false;
|
||||
const aEnd = /[0-9\)\!]/.test(a);
|
||||
const bStart = /[\(0-9a-zA-Z]/.test(b) && b !== "!";
|
||||
return aEnd && bStart;
|
||||
}
|
||||
|
||||
function append(s) {
|
||||
if (shouldInsertMul(s)) exprStr += "*";
|
||||
exprStr += s;
|
||||
updateView();
|
||||
}
|
||||
|
||||
function inputDigit(d) {
|
||||
append(d);
|
||||
}
|
||||
|
||||
function inputDot() {
|
||||
append(".");
|
||||
}
|
||||
|
||||
function doEval() {
|
||||
try {
|
||||
const v = evaluate(exprStr);
|
||||
lastResult = v;
|
||||
exprStr = "";
|
||||
updateView();
|
||||
} catch {
|
||||
lastResult = NaN;
|
||||
updateView();
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
exprStr = "";
|
||||
lastResult = 0;
|
||||
updateView();
|
||||
}
|
||||
|
||||
function back() {
|
||||
exprStr = exprStr.slice(0, -1);
|
||||
updateView();
|
||||
}
|
||||
|
||||
function insertAns() {
|
||||
append("ANS");
|
||||
}
|
||||
|
||||
degBtn.addEventListener("click", function () {
|
||||
degMode = !degMode;
|
||||
degBtn.textContent = degMode ? "DEG" : "RAD";
|
||||
degBtn.title = degMode
|
||||
? "Angles in degrees (click for radians)"
|
||||
: "Angles in radians (click for degrees)";
|
||||
});
|
||||
|
||||
document.querySelector(".calc").addEventListener("click", function (e) {
|
||||
const btn = e.target.closest("button");
|
||||
if (!btn) return;
|
||||
if (btn === degBtn) return;
|
||||
|
||||
const ap = btn.getAttribute("data-append");
|
||||
const d = btn.getAttribute("data-digit");
|
||||
const action = btn.getAttribute("data-action");
|
||||
|
||||
if (action === "equals") {
|
||||
doEval();
|
||||
return;
|
||||
}
|
||||
if (d !== null) {
|
||||
inputDigit(d);
|
||||
return;
|
||||
}
|
||||
if (ap !== null) {
|
||||
append(ap);
|
||||
return;
|
||||
}
|
||||
if (action === "clear") clear();
|
||||
else if (action === "back") back();
|
||||
else if (action === "dot") inputDot();
|
||||
else if (action === "ans") insertAns();
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key >= "0" && e.key <= "9") {
|
||||
e.preventDefault();
|
||||
inputDigit(e.key);
|
||||
} else if (e.key === ".") {
|
||||
e.preventDefault();
|
||||
inputDot();
|
||||
} else if (e.key === "+") {
|
||||
e.preventDefault();
|
||||
append("+");
|
||||
} else if (e.key === "-") {
|
||||
e.preventDefault();
|
||||
append("-");
|
||||
} else if (e.key === "*") {
|
||||
e.preventDefault();
|
||||
append("*");
|
||||
} else if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
append("/");
|
||||
} else if (e.key === "^") {
|
||||
e.preventDefault();
|
||||
append("^");
|
||||
} else if (e.key === "(") {
|
||||
e.preventDefault();
|
||||
append("(");
|
||||
} else if (e.key === ")") {
|
||||
e.preventDefault();
|
||||
append(")");
|
||||
} else if (e.key === "Enter" || (e.key === "=" && !e.shiftKey)) {
|
||||
e.preventDefault();
|
||||
doEval();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
clear();
|
||||
} else if (e.key === "Backspace") {
|
||||
e.preventDefault();
|
||||
back();
|
||||
}
|
||||
});
|
||||
|
||||
updateView();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
where docker >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Docker not found. Install Docker Desktop.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Calculator: http://localhost:8888
|
||||
start "" "http://localhost:8888"
|
||||
docker compose up
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Docker not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Calculator: http://localhost:8888"
|
||||
(
|
||||
sleep 2
|
||||
command -v xdg-open >/dev/null 2>&1 && xdg-open "http://localhost:8888" 2>/dev/null || true
|
||||
command -v open >/dev/null 2>&1 && open "http://localhost:8888" 2>/dev/null || true
|
||||
) &
|
||||
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
exec docker compose up
|
||||
fi
|
||||
if command -v docker-compose >/dev/null 2>&1; then
|
||||
exec docker-compose up
|
||||
fi
|
||||
|
||||
echo "Docker Compose not found."
|
||||
exit 1
|
||||
@@ -0,0 +1,23 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
where python >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
set "PY=python"
|
||||
) else (
|
||||
where py >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Python not found. Install from https://www.python.org/
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
set "PY=py"
|
||||
)
|
||||
|
||||
echo Calculator: http://localhost:8888
|
||||
start "" "http://localhost:8888"
|
||||
"%PY%" -m http.server 8888 --bind 127.0.0.1
|
||||
|
||||
echo.
|
||||
pause
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
PY=python3
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
PY=python
|
||||
else
|
||||
echo "Python not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Calculator: http://localhost:8888"
|
||||
(
|
||||
sleep 1
|
||||
command -v xdg-open >/dev/null 2>&1 && xdg-open "http://localhost:8888" 2>/dev/null || true
|
||||
command -v open >/dev/null 2>&1 && open "http://localhost:8888" 2>/dev/null || true
|
||||
) &
|
||||
|
||||
exec "$PY" -m http.server 8888 --bind 127.0.0.1
|
||||
Reference in New Issue
Block a user