Export template source from 3ac0870810e7bf32e6f262f4b81f11ce7470e784:quixos-instance/packages/project-react-runtime
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user