# Example: Fullstack Todo App

Complete fullstack app: backend nodes, def:priv endpoints, hook, components, auth, file-based routing.

## Project Structure

```
todo-app/
├── jac.toml
├── main.jac                      # ALL backend + cl { app() }
├── components/
│   ├── Layout.cl.jac             # Root layout
│   ├── Header.cl.jac             # Nav bar
│   ├── TodoList.cl.jac           # List + add form (calls hook)
│   ├── TodoItem.cl.jac           # Single item display
│   └── AddTodoForm.cl.jac        # Input form
├── hooks/
│   └── useTodos.cl.jac           # Data hook (sv import + state)
├── pages/
│   ├── layout.jac                # Root layout with nav
│   ├── (public)/
│   │   ├── login.jac             # Login page
│   │   └── signup.jac            # Signup page
│   └── (auth)/
│       ├── layout.jac            # Auth guard
│       └── dashboard.jac         # Dashboard
└── styles/global.css
```

## main.jac — Backend + Entry

```jac
import from uuid { uuid4 }
import from datetime { datetime }

node Todo {
    has id: str = "";
    has title: str = "";
    has completed: bool = False;
    has created_at: str = "";

    def postinit {
        if not self.id { self.id = str(uuid4()); }
        if not self.created_at { self.created_at = datetime.now().isoformat(); }
    }
}

def:priv get_todos() -> list {
    return [{"id": t.id, "title": t.title, "completed": t.completed}
            for t in [root-->][?:Todo]];
}

def:priv add_todo(title: str) -> dict {
    todo = (root ++> Todo(title=title))[0];
    return {"id": todo.id, "title": todo.title, "completed": False};
}

def:priv toggle_todo(id: str) -> dict {
    for todo in [root-->][?:Todo] {
        if todo.id == id {
            todo.completed = not todo.completed;
            return {"id": todo.id, "completed": todo.completed};
        }
    }
    return {"error": "not found"};
}

def:priv delete_todo(id: str) -> dict {
    for todo in [root-->][?:Todo] {
        if todo.id == id {
            root del--> todo;
            return {"deleted": id};
        }
    }
    return {"error": "not found"};
}

cl import from .components.Layout { Layout }
cl {
    def:pub app() -> JsxElement { return <Layout />; }
}
```

## hooks/useTodos.cl.jac — Data Hook

```jac
sv import from ..main { get_todos, add_todo, toggle_todo, delete_todo }

def:pub useTodos() -> dict {
    has todos: list = [];
    has loading: bool = True;
    has error: str = "";

    async can with entry {
        try {
            result = await get_todos();
            todos = result or [];
        } except Exception as e {
            error = str(e);
        }
        loading = False;
    }

    async def handleAdd(title: str) -> None {
        if not title { return; }
        result = await add_todo(title);
        if result and not result.error {
            todos = todos + [result];
        }
    }

    async def handleToggle(id: str) -> None {
        result = await toggle_todo(id);
        if result and not result.error {
            todos = [
                ({"id": t["id"], "title": t["title"],
                  "completed": result["completed"]}
                 if t["id"] == id else t)
                for t in todos
            ];
        }
    }

    async def handleDelete(id: str) -> None {
        result = await delete_todo(id);
        if result and not result.error {
            todos = [t for t in todos if t["id"] != id];
        }
    }

    return {
        "todos": todos, "loading": loading, "error": error,
        "handleAdd": handleAdd, "handleToggle": handleToggle,
        "handleDelete": handleDelete
    };
}
```

## components/Layout.cl.jac — Root Layout

```jac
import "..styles.global.css";
import from .Header { Header }
import from .TodoList { TodoList }

def:pub Layout() -> JsxElement {
    return (
        <div className="min-h-screen bg-gray-50">
            <Header title="Todo App" />
            <main className="max-w-2xl mx-auto px-4 py-8">
                <TodoList />
            </main>
        </div>
    );
}
```

## components/Header.cl.jac

```jac
def:pub Header(props: dict) -> JsxElement {
    title = props.title or "";
    return (
        <header className="border-b px-6 py-4">
            <h1 className="text-xl font-bold">{title}</h1>
        </header>
    );
}
```

## components/TodoItem.cl.jac

```jac
def:pub TodoItem(props: dict) -> JsxElement {
    todo = props.todo or {};
    onToggle = props.onToggle or None;
    onDelete = props.onDelete or None;
    todoId = todo["id"] or "";
    title = todo["title"] or "";
    completed = todo["completed"] or False;

    def handle_toggle() -> None {
        if onToggle { onToggle(todoId); }
    }

    def handle_delete() -> None {
        if onDelete { onDelete(todoId); }
    }

    return (
        <div className="flex items-center gap-3 p-3 border rounded-lg">
            <button onClick={handle_toggle} className="w-5 h-5 rounded border-2">
                {completed and "✓"}
            </button>
            <span class={completed and "flex-1 line-through text-gray-400" or "flex-1"}>
                {title}
            </span>
            <button onClick={handle_delete} className="text-red-500">×</button>
        </div>
    );
}
```

## components/AddTodoForm.cl.jac

```jac
def:pub AddTodoForm(props: dict) -> JsxElement {
    onAdd = props.onAdd or None;
    has inputValue: str = "";

    def handleSubmit(e: any) -> None {
        e.preventDefault();
        if inputValue and onAdd {
            onAdd(inputValue);
            inputValue = "";
        }
    }

    def handle_input(e: any) -> None {
        inputValue = e.target.value;
    }

    return (
        <form onSubmit={handleSubmit} className="flex gap-2">
            <input value={inputValue} onChange={handle_input}
                placeholder="What needs to be done?"
                className="flex-1 px-4 py-2 border rounded-lg" />
            <button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded-lg">+</button>
        </form>
    );
}
```

## components/TodoList.cl.jac

```jac
import from .TodoItem { TodoItem }
import from .AddTodoForm { AddTodoForm }
import from ..hooks.useTodos { useTodos }

def:pub TodoList() -> JsxElement {
    todoData = useTodos();
    todos = todoData["todos"] or [];

    if todoData["loading"] {
        return <p className="text-gray-400 text-center py-8">Loading...</p>;
    }

    return (
        <div>
            {todoData["error"] and (<p className="text-red-500 mb-4">{todoData["error"]}</p>)}
            <div className="mb-6"><AddTodoForm onAdd={todoData["handleAdd"]} /></div>
            {len(todos) == 0 and (<p className="text-gray-400 text-center py-8">No todos yet.</p>)}
            <div className="space-y-2">
                {[
                    <TodoItem key={todo["id"]} todo={todo}
                        onToggle={todoData["handleToggle"]}
                        onDelete={todoData["handleDelete"]} />
                    for todo in todos
                ]}
            </div>
        </div>
    );
}
```

## pages/(public)/login.jac — Login Page

```jac
cl import from "@jac/runtime" { jacLogin, useNavigate, Link }

cl {
    def:pub page() -> JsxElement {
        has username: str = "";
        has password: str = "";
        has error: str = "";
        has loading: bool = False;
        navigate = useNavigate();

        async def handleLogin(e: any) -> None {
            e.preventDefault();
            loading = True;
            error = "";
            success = await jacLogin(username, password);
            if success { navigate("/"); }
            else { error = "Invalid credentials"; }
            loading = False;
        }

        def handle_user(e: any) -> None { username = e.target.value; }
        def handle_pass(e: any) -> None { password = e.target.value; }

        return (
            <div className="max-w-sm mx-auto">
                <h1 className="text-2xl font-bold mb-6">Login</h1>
                {error and <div className="p-3 mb-4 text-red-500">{error}</div>}
                <form onSubmit={handleLogin} className="space-y-4">
                    <input value={username} onChange={handle_user} placeholder="Username"
                        className="w-full px-4 py-2 border rounded-lg" />
                    <input type="password" value={password} onChange={handle_pass}
                        placeholder="Password" className="w-full px-4 py-2 border rounded-lg" />
                    <button type="submit" disabled={loading}
                        className="w-full px-4 py-2 bg-blue-500 text-white rounded-lg">
                        {loading and "Logging in..." or "Login"}
                    </button>
                </form>
                <p className="text-center mt-4 text-sm text-gray-500">
                    No account? <Link to="/signup">Sign up</Link>
                </p>
            </div>
        );
    }
}
```

## pages/(public)/signup.jac — Signup Page

```jac
cl import from "@jac/runtime" { jacSignup, useNavigate, Link }

cl {
    def:pub page() -> JsxElement {
        has username: str = "";
        has password: str = "";
        has error: str = "";
        has loading: bool = False;
        navigate = useNavigate();

        async def handleSignup(e: any) -> None {
            e.preventDefault();
            error = "";
            if not username.strip() or not password {
                error = "Please fill in all fields";
                return;
            }
            loading = True;
            result = await jacSignup(username, password);
            loading = False;
            if result["success"] {
                navigate("/");
            } else {
                error = result["error"] or "Signup failed";
            }
        }

        def handle_user(e: any) -> None { username = e.target.value; }
        def handle_pass(e: any) -> None { password = e.target.value; }

        return (
            <div className="max-w-sm mx-auto">
                <h1 className="text-2xl font-bold mb-6">Create Account</h1>
                {error and <div className="p-3 mb-4 text-red-500">{error}</div>}
                <form onSubmit={handleSignup} className="space-y-4">
                    <input value={username} onChange={handle_user} placeholder="Username"
                        className="w-full px-4 py-2 border rounded-lg" />
                    <input type="password" value={password} onChange={handle_pass}
                        placeholder="Password" className="w-full px-4 py-2 border rounded-lg" />
                    <button type="submit" disabled={loading}
                        className="w-full px-4 py-2 bg-blue-500 text-white rounded-lg">
                        {loading and "Creating account..." or "Sign Up"}
                    </button>
                </form>
                <p className="text-center mt-4 text-sm text-gray-500">
                    Already have an account? <Link to="/login">Login</Link>
                </p>
            </div>
        );
    }
}
```

## pages/(auth)/layout.jac — Auth Guard

```jac
cl import from "@jac/runtime" { AuthGuard, Outlet }

cl {
    def:pub layout() -> JsxElement {
        return <AuthGuard redirect="/login"><Outlet /></AuthGuard>;
    }
}
```
