This commit is contained in:
metacryst
2026-04-28 20:05:00 -05:00
commit 0d6c7683ff
123 changed files with 20922 additions and 0 deletions

144
tasks/desktop/tasks.js Normal file
View File

@@ -0,0 +1,144 @@
import "../../components/AppTitle.js"
import taskServer from "/tasks/@server/tasks.js"
class Tasks extends Shadow {
tasks = []
newTitle = ""
render() {
VStack(() => {
AppTitle("Tasks")
.marginTop(1, em)
.marginBottom(2, em)
.marginLeft(7, vw)
VStack(() => {
this.tasks.forEach((task) => {
HStack(() => {
// Circular checkbox
VStack(() => {})
.width(1.1, em).height(1.1, em)
.minWidth(1.1, em)
.borderRadius(50, pct)
.border(task.done ? "none" : "1.5px solid var(--divider)")
.background(task.done ? "var(--accent)" : "transparent")
.cursor("pointer")
.flexShrink(0)
.onClick(async (end) => {
if(end) {
const updated = await taskServer.updateTaskDone(task.id, !task.done)
console.log(updated)
if (updated && updated.id) {
const idx = this.tasks.findIndex(t => t.id === task.id)
if (idx >= 0) this.tasks[idx] = updated
this.rerender()
}
}
})
p(`#${task.id}`)
.margin(0)
.fontSize(0.75, em)
.color("var(--headertext)")
.opacity(0.4)
.flexShrink(0)
input("", "100%")
.attr({ value: task.title, placeholder: "Task title" })
.background("transparent")
.border("none")
.outline("none")
.fontSize(0.9, em)
.color(task.done ? "var(--headertext)" : "var(--text)")
.textDecoration(task.done ? "line-through" : "none")
.opacity(task.done ? 0.5 : 1)
.padding(0)
.onBlur(async function () {
const newVal = this.value.trim()
if (newVal && newVal !== task.title) {
await taskServer.editTaskTitle(task.id, newVal)
task.title = newVal
}
})
.onKeyDown(function (e) {
if (e.key === "Enter") this.blur()
})
// Delete button
p("×")
.margin(0)
.fontSize(1.1, em)
.color("var(--headertext)")
.opacity(0.3)
.cursor("pointer")
.flexShrink(0)
.onHover(function (hovering) {
this.style.opacity = hovering ? "1" : "0.3"
this.style.color = hovering ? "var(--quillred)" : "var(--headertext)"
})
.onClick(async () => {
await taskServer.deleteTask(task.id)
this.tasks = this.tasks.filter(t => t.id !== task.id)
this.rerender()
})
})
.gap(0.65, em)
.alignItems("center")
.paddingVertical(0.4, em)
.paddingHorizontal(0.5, em)
.marginRight(0)
})
})
.width(70, pct)
.marginLeft(7, vw)
.border("1px solid var(--divider)")
.borderRadius(8, px)
.overflow("hidden")
// Add task input
input("", "70%")
.attr({ placeholder: "New task..." })
.marginLeft(7, vw)
.marginTop(0.5, em)
.padding(0.5, em)
.paddingHorizontal(0.75, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(8, px)
.outline("none")
.fontSize(0.9, em)
.boxSizing("border-box")
.color("var(--text)")
.onKeyDown(async (e) => {
if (e.key === "Enter") {
console.log("enter")
const val = e.target.value.trim()
if (!val) return
const task = await taskServer.addTask(global.currentNetwork.id, val)
if (task && task.id) {
this.tasks.push(task)
e.target.value = ""
this.rerender()
} else {
if(task.error) console.error("Error making task: ", task.error)
}
}
})
})
.onAppear(async () => {
const tasks = await taskServer.getTasks(global.currentNetwork.id)
if (tasks && !tasks.error && tasks.length !== this.tasks.length) {
this.tasks = tasks
this.rerender()
}
})
.gap(0)
.paddingTop(2, pct)
.paddingBottom(4, pct)
.width(80, vw)
.height(100, pct)
.overflow("auto")
}
}
register(Tasks)

4
tasks/icons/tasks.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="125" height="131" viewBox="0 0 125 131" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M123.931 0.6264C122.635 -0.348 120.853 -0.1856 119.881 1.1136L60.4266 76.6296L30.7806 49.6712C29.3226 48.372 27.2166 48.2096 25.5966 49.3464C23.6526 50.6456 23.1666 53.244 24.4626 55.1928L53.6226 99.0408C54.4326 100.178 55.5666 101.314 56.8626 101.964C61.5606 104.725 67.3926 103.101 70.1466 98.3912L70.4706 97.904L124.579 4.3616C125.227 3.0624 125.065 1.4384 123.931 0.6264Z" fill="black"/>
<path d="M107.532 109.271C107.532 114.955 100.926 121.736 95.256 121.736H20.736C15.066 121.736 7.53223 114.793 7.53223 109.109V34.7296C7.53223 29.0456 15.066 21.7363 20.736 21.7363H87.5322L96.39 13.9424C96.066 13.9424 95.58 13.9424 95.256 13.9424H20.736C9.396 13.9424 0 23.1992 0 34.7296V109.271C0 120.639 9.234 130.058 20.736 130.058H95.094C106.434 130.058 115.83 120.802 115.83 109.271V40.2512L107.532 58.7363V109.271Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 945 B

View File

@@ -0,0 +1,4 @@
<svg width="125" height="131" viewBox="0 0 125 131" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M123.931 0.6264C122.635 -0.348 120.853 -0.1856 119.881 1.1136L60.4266 76.6296L30.7806 49.6712C29.3226 48.372 27.2166 48.2096 25.5966 49.3464C23.6526 50.6456 23.1666 53.244 24.4626 55.1928L53.6226 99.0408C54.4326 100.178 55.5666 101.314 56.8626 101.964C61.5606 104.725 67.3926 103.101 70.1466 98.3912L70.4706 97.904L124.579 4.3616C125.227 3.0624 125.065 1.4384 123.931 0.6264Z" fill="#FFE9C8"/>
<path d="M107.532 109.271C107.532 114.955 100.926 121.736 95.256 121.736H20.736C15.066 121.736 7.53223 114.793 7.53223 109.109V34.7296C7.53223 29.0456 15.066 21.7363 20.736 21.7363H87.5322L96.39 13.9424C96.066 13.9424 95.58 13.9424 95.256 13.9424H20.736C9.396 13.9424 0 23.1992 0 34.7296V109.271C0 120.639 9.234 130.058 20.736 130.058H95.094C106.434 130.058 115.83 120.802 115.83 109.271V40.2512L107.532 58.7363V109.271Z" fill="#FFE9C8"/>
</svg>

After

Width:  |  Height:  |  Size: 949 B

47
tasks/server/functions.js Normal file
View File

@@ -0,0 +1,47 @@
export async function getTasks(networkId) {
const tasks = await this.sql`
SELECT * FROM tasks.tasks
WHERE network_id = ${networkId}
AND is_active = true
ORDER BY created ASC
`;
return tasks
}
export async function addTask(networkId, title) {
const [task] = await this.sql`
INSERT INTO tasks.tasks (title, network_id)
VALUES (${title}, ${networkId})
RETURNING *
`;
return task
}
export async function updateTaskDone(taskId, done) {
const [task] = await this.sql`
UPDATE tasks.tasks
SET done = ${done}
WHERE id = ${taskId}
RETURNING *
`;
return task
}
export async function editTaskTitle(taskId, title) {
const [task] = await this.sql`
UPDATE tasks.tasks
SET title = ${title}
WHERE id = ${taskId}
RETURNING *
`;
return task
}
export async function deleteTask(taskId) {
await this.sql`
UPDATE tasks.tasks
SET is_active = false
WHERE id = ${taskId}
`;
return { ok: true }
}

152
tasks/tasks.js Normal file
View File

@@ -0,0 +1,152 @@
import taskServer from "/tasks/@server/tasks.js"
class Tasks extends Shadow {
tasks = []
render() {
VStack(() => {
// Task list — full-bleed on mobile, no inset borders
VStack(() => {
if (this.tasks.length === 0) {
p("No tasks yet")
.margin(0)
.padding(2, em)
.textAlign("center")
.color("var(--headertext)")
.opacity(0.4)
.fontSize(0.95, em)
} else {
this.tasks.forEach((task, i) => {
HStack(() => {
// Larger circular checkbox — 1.6em for thumb-friendly target
VStack(() => {})
.width(1.6, em).height(1.6, em)
.minWidth(1.6, em)
.borderRadius(50, pct)
.border(task.done ? "none" : "2px solid var(--divider)")
.background(task.done ? "var(--accent)" : "transparent")
.cursor("pointer")
.flexShrink(0)
.transition("all 0.15s ease")
.onClick(async (end) => {
if (end) {
const updated = await taskServer.updateTaskDone(task.id, !task.done)
if (updated && updated.id) {
const idx = this.tasks.findIndex(t => t.id === task.id)
if (idx >= 0) this.tasks[idx] = updated
this.rerender()
}
}
})
// Title input — takes remaining width
input("", "100%")
.attr({ value: task.title, placeholder: "Task title" })
.background("transparent")
.border("none")
.outline("none")
.fontSize(1.05, em)
.color(task.done ? "var(--headertext)" : "var(--text)")
.textDecoration(task.done ? "line-through" : "none")
.opacity(task.done ? 0.5 : 1)
.padding(0)
.paddingVertical(0.25, em)
.onBlur(async function () {
const newVal = this.value.trim()
if (newVal && newVal !== task.title) {
await taskServer.editTaskTitle(task.id, newVal)
task.title = newVal
}
})
.onKeyDown(function (e) {
if (e.key === "Enter") this.blur()
})
// Delete button — bigger hit area on mobile
VStack(() => {
p("×")
.margin(0)
.fontSize(1.4, em)
.color("var(--headertext)")
.opacity(0.35)
.lineHeight(1)
})
.width(2.2, em).height(2.2, em)
.minWidth(2.2, em)
.alignItems("center")
.justifyContent("center")
.cursor("pointer")
.flexShrink(0)
.onClick(async () => {
await taskServer.deleteTask(task.id)
this.tasks = this.tasks.filter(t => t.id !== task.id)
this.rerender()
})
})
.gap(0.85, em)
.alignItems("center")
.paddingVertical(0.85, em)
.paddingHorizontal(5, vw)
.borderBottom(i < this.tasks.length - 1 ? "1px solid var(--divider)" : "none")
})
}
})
.width(100, pct)
.borderTop("1px solid var(--divider)")
.borderBottom("1px solid var(--divider)")
})
.onAppear(async () => {
const tasks = await taskServer.getTasks(global.currentNetwork.id)
if (tasks && !tasks.error && tasks.length !== this.tasks.length) {
this.tasks = tasks
this.rerender()
}
})
.gap(0)
.paddingBottom(6, em) // leave room for sticky input below
.width(100, vw)
.height(100, pct)
.overflow("auto")
// Sticky bottom add-task bar — anchored above the keyboard area, easy thumb reach
HStack(() => {
input("", "100%")
.attr({ placeholder: "New task..." })
.padding(0.85, em)
.paddingHorizontal(1, em)
.background("var(--darkaccent)")
.border("1px solid var(--divider)")
.borderRadius(999, px)
.outline("none")
.fontSize(1, em)
.boxSizing("border-box")
.color("var(--text)")
.onKeyDown(async (e) => {
if (e.key === "Enter") {
const val = e.target.value.trim()
if (!val) return
const task = await taskServer.addTask(global.currentNetwork.id, val)
if (task && task.id) {
this.tasks.push(task)
e.target.value = ""
this.rerender()
} else if (task && task.error) {
console.error("Error making task:", task.error)
}
}
})
})
.position("fixed")
.bottom(0, px)
.left(0, px)
.right(0, px)
.padding(0.75, em)
.paddingHorizontal(5, vw)
.paddingBottom("calc(0.75em + env(safe-area-inset-bottom))")
.background("var(--background)")
.borderTop("1px solid var(--divider)")
}
}
register(Tasks)