inital Jack

This commit is contained in:
2026-03-30 15:42:39 -04:00
commit cd46057cd2
11 changed files with 837 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
.git/
*.sh
*.bat
README.md
compose.yaml
Dockerfile
+11
View File
@@ -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;"]
+59
View File
@@ -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.
+7
View File
@@ -0,0 +1,7 @@
services:
web:
build: .
image: calculator:latest
ports:
- "8888:80"
restart: unless-stopped
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+671
View File
@@ -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"></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>
+14
View File
@@ -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
+25
View File
@@ -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
+23
View File
@@ -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
+21
View File
@@ -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