Developers
Example app: Tow Board
A complete UplinkOS app you can copy. Tow Board tracks tow jobs: add a plate and street, mark it done, and switch between Open and Done.
#Try it in Studio
- Make a project. In Studio, press New project and pick Empty.
- Add the four files. Make each file below with New file and paste its code in.
- Run it. Type
runin the terminal. Tow Board installs on the test phone and opens.
You can also open it in your browser to see how it looks.
#uplink.json
Optional. Sets the name and the page the app opens on.
uplink.json
{
"name": "Tow Board",
"entry": "index.html"
}#index.html
The page the app opens on. It links its CSS and JavaScript with relative paths.
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tow Board</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="app">
<header>
<h1>Tow Board</h1>
<p class="sub" id="summary">No jobs yet</p>
</header>
<form id="add" class="card" autocomplete="off">
<input name="plate" placeholder="Plate" maxlength="8" required>
<input name="street" placeholder="Street" maxlength="40" required>
<button type="submit">Add job</button>
</form>
<nav class="tabs">
<button type="button" data-show="open" class="on">Open</button>
<button type="button" data-show="done">Done</button>
</nav>
<ul id="jobs" class="list"></ul>
<p id="empty" class="sub">Nothing here.</p>
</main>
<script src="app.js"></script>
</body>
</html>#style.css
Looks like the rest of the phone, in light and dark.
style.css
:root {
color-scheme: light dark;
--bg: #f2f2f7;
--card: #fff;
--text: #000;
--muted: #8e8e93;
--accent: #ff9f0a;
}
@media (prefers-color-scheme: dark) {
:root { --bg: #000; --card: #1c1c1e; --text: #fff; }
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
background: var(--bg);
color: var(--text);
}
.app { padding: 20px 16px; display: grid; gap: 14px; }
h1 { margin: 0; font-size: 30px; }
.sub { margin: 0; color: var(--muted); }
.card {
display: grid;
gap: 8px;
padding: 12px;
border-radius: 14px;
background: var(--card);
}
input, button { font: inherit; border: 0; border-radius: 10px; }
input { padding: 12px; background: var(--bg); color: var(--text); }
button { padding: 12px; background: var(--accent); color: #000; font-weight: 600; }
.tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.tabs button { background: var(--card); color: var(--muted); }
.tabs button.on { background: var(--accent); color: #000; }
.list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.job {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 14px;
background: var(--card);
}
.job b { display: block; font-family: ui-monospace, monospace; }
.job span { color: var(--muted); font-size: 14px; }
.job div { flex: 1; }
.job button { padding: 8px 12px; }#app.js
Keeps the jobs in memory while the app is open. Browser storage is blocked on the phone, so a real app would save jobs on its own server.
app.js
const state = {
jobs: [],
show: "open",
};
const $ = (sel) => document.querySelector(sel);
function addJob(plate, street) {
state.jobs.unshift({
id: Date.now(),
plate: plate.trim().toUpperCase(),
street: street.trim(),
done: false,
at: new Date(),
});
render();
}
function toggleJob(id) {
const job = state.jobs.find((j) => j.id === id);
if (job) job.done = !job.done;
render();
}
function jobRow(job) {
const li = document.createElement("li");
li.className = "job";
const info = document.createElement("div");
const plate = document.createElement("b");
plate.textContent = job.plate;
const where = document.createElement("span");
where.textContent = `${job.street} · ${job.at.toLocaleTimeString()}`;
info.append(plate, where);
const btn = document.createElement("button");
btn.type = "button";
btn.textContent = job.done ? "Reopen" : "Done";
btn.addEventListener("click", () => toggleJob(job.id));
li.append(info, btn);
return li;
}
function render() {
const open = state.jobs.filter((j) => !j.done).length;
const shown = state.jobs.filter((j) => j.done === (state.show === "done"));
$("#summary").textContent = state.jobs.length
? `${open} open · ${state.jobs.length - open} done`
: "No jobs yet";
$("#jobs").replaceChildren(...shown.map(jobRow));
$("#empty").hidden = shown.length > 0;
document.querySelectorAll("[data-show]").forEach((b) => {
b.classList.toggle("on", b.dataset.show === state.show);
});
}
$("#add").addEventListener("submit", (e) => {
e.preventDefault();
const form = e.target;
addJob(form.plate.value, form.street.value);
form.reset();
console.log("Job added", state.jobs[0]);
});
document.querySelectorAll("[data-show]").forEach((b) => {
b.addEventListener("click", () => {
state.show = b.dataset.show;
render();
});
});
render();
console.log("Tow Board started");#Make it yours
- Add an
icon.pngand put"icon": "icon.png"inuplink.json. - Save jobs on your own server so they are still there next time.
- When it works, type
packageand submit it.