186 lines
5.8 KiB
TypeScript
186 lines
5.8 KiB
TypeScript
import { useState } from "react";
|
|
import {
|
|
h,
|
|
invokeCapability,
|
|
useLiveField,
|
|
type LiveFieldProp,
|
|
type ObjectRef,
|
|
type ReactComponentImplementationProps,
|
|
} from "@quixos/web-studio-react-runtime";
|
|
import {
|
|
TodoComponent,
|
|
type TodoComponentProps,
|
|
type TodoReactComponentAction,
|
|
} from "./gen/quixos.todo.TodoReactComponent.react.js";
|
|
import type { ProjectReactComponentRenderProps } from "./gen/quixos.todo.ProjectReactComponent.react.js";
|
|
|
|
type TaskRef = TodoComponentProps["forObject"];
|
|
|
|
type ProjectCaminoProps = {
|
|
project_id: ObjectRef<"atom:project">;
|
|
name: LiveFieldProp<string>;
|
|
tasks: TaskRef[];
|
|
};
|
|
|
|
type ProjectComponentProps = ReactComponentImplementationProps<
|
|
ProjectCaminoProps,
|
|
ProjectReactComponentRenderProps,
|
|
Record<string, never>
|
|
>;
|
|
|
|
const colors = {
|
|
ink: "#28362d",
|
|
muted: "#72786f",
|
|
cream: "#fffdf5",
|
|
paper: "#f6f0df",
|
|
line: "#d9d0ba",
|
|
moss: "#5f7f63",
|
|
honey: "#d99b3d",
|
|
};
|
|
|
|
export default function ProjectComponent({ camino, render }: ProjectComponentProps) {
|
|
const [name, setName] = useLiveField(camino.name);
|
|
const [selectedTask, setSelectedTask] = useState<TaskRef | null>(null);
|
|
const [newTaskTitle, setNewTaskTitle] = useState("");
|
|
const [addingTask, setAddingTask] = useState(false);
|
|
const [addTaskError, setAddTaskError] = useState("");
|
|
const handleTaskAction = (task: TaskRef, action: TodoReactComponentAction) => {
|
|
if (action.type === "SELECT") setSelectedTask(task);
|
|
};
|
|
const addTask = async () => {
|
|
if (addingTask || !newTaskTitle.trim()) return;
|
|
setAddingTask(true);
|
|
setAddTaskError("");
|
|
try {
|
|
await invokeCapability(
|
|
camino.project_id,
|
|
"interface:project-actions@1",
|
|
"operation:project-actions:add-task",
|
|
newTaskTitle,
|
|
);
|
|
setNewTaskTitle("");
|
|
} catch (error) {
|
|
setAddTaskError(error instanceof Error ? error.message : String(error));
|
|
} finally {
|
|
setAddingTask(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<section style={{
|
|
display: "grid",
|
|
gridTemplateRows: "auto auto minmax(0, 1fr)",
|
|
gap: "12px",
|
|
width: "100%",
|
|
height: "100%",
|
|
minHeight: 0,
|
|
padding: render.compact ? "11px" : "14px",
|
|
color: colors.ink,
|
|
fontFamily: "Avenir Next, Nunito, ui-sans-serif, system-ui, sans-serif",
|
|
}}>
|
|
<header style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px" }}>
|
|
<div style={{ display: "grid", gap: "2px", minWidth: 0, flex: 1 }}>
|
|
<input
|
|
aria-label="Project name"
|
|
value={name}
|
|
onInput={(event) => void setName(event.currentTarget.value)}
|
|
style={{
|
|
minWidth: 0,
|
|
width: "100%",
|
|
border: "none",
|
|
borderBottom: `1px solid ${colors.line}`,
|
|
background: "transparent",
|
|
color: colors.ink,
|
|
fontFamily: "Georgia, ui-serif, serif",
|
|
fontSize: render.compact ? "17px" : "22px",
|
|
fontWeight: 700,
|
|
padding: "3px 0 5px",
|
|
}}
|
|
/>
|
|
<span style={{ color: colors.muted, fontSize: "11px" }}>A little clearing for useful things.</span>
|
|
</div>
|
|
<span style={{
|
|
flex: "0 0 auto",
|
|
border: `1px solid ${colors.line}`,
|
|
borderRadius: "8px",
|
|
background: colors.paper,
|
|
color: colors.moss,
|
|
fontSize: "11px",
|
|
fontWeight: 800,
|
|
padding: "5px 8px",
|
|
}}>
|
|
{camino.tasks.length} {camino.tasks.length === 1 ? "task" : "tasks"}
|
|
</span>
|
|
</header>
|
|
|
|
<form
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
void addTask();
|
|
}}
|
|
style={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr) auto", gap: "8px" }}
|
|
>
|
|
<input
|
|
aria-label="New task title"
|
|
value={newTaskTitle}
|
|
onInput={(event) => setNewTaskTitle(event.currentTarget.value)}
|
|
placeholder="What wants doing?"
|
|
disabled={addingTask}
|
|
style={{ minWidth: 0, border: `1px solid ${colors.line}`, borderRadius: "9px", background: "#fff", padding: "8px 10px" }}
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={addingTask || newTaskTitle.trim().length === 0}
|
|
style={{
|
|
border: `1px solid ${colors.moss}`,
|
|
borderRadius: "9px",
|
|
background: colors.moss,
|
|
color: "#fffdf5",
|
|
cursor: "pointer",
|
|
fontSize: "12px",
|
|
fontWeight: 750,
|
|
padding: "8px 12px",
|
|
}}
|
|
>
|
|
{addingTask ? "Planting…" : "Add task"}
|
|
</button>
|
|
{addTaskError ? <div role="alert" style={{ gridColumn: "1 / -1", color: "#9f4741", fontSize: "11px" }}>{addTaskError}</div> : null}
|
|
</form>
|
|
|
|
<div style={{
|
|
display: "grid",
|
|
alignContent: "start",
|
|
gap: "9px",
|
|
minHeight: 0,
|
|
overflow: "auto",
|
|
paddingRight: "3px",
|
|
}}>
|
|
{camino.tasks.length === 0 ? (
|
|
<div style={{
|
|
display: "grid",
|
|
placeItems: "center",
|
|
minHeight: render.compact ? "80px" : "140px",
|
|
border: `1px dashed ${colors.line}`,
|
|
borderRadius: "12px",
|
|
background: colors.paper,
|
|
color: colors.muted,
|
|
fontSize: "12px",
|
|
textAlign: "center",
|
|
}}>
|
|
Your project is quiet. Add the first task when it arrives.
|
|
</div>
|
|
) : camino.tasks.map((task) => (
|
|
<TodoComponent
|
|
key={task}
|
|
forObject={task}
|
|
selected={selectedTask === task}
|
|
compact={render.compact ?? true}
|
|
onAction={(action) => handleTaskAction(task, action)}
|
|
fallback={<div style={{ color: colors.muted, fontSize: "11px" }}>Waking task…</div>}
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|