|
@@ -0,0 +1,485 @@
|
|
|
|
|
+using Astralis;
|
|
|
|
|
+using Invercargill;
|
|
|
|
|
+using Invercargill.DataStructures;
|
|
|
|
|
+using Inversion;
|
|
|
|
|
+using Spry;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * TodoComponent Example
|
|
|
|
|
+ *
|
|
|
|
|
+ * Demonstrates using the Spry.Component class to build a todo list
|
|
|
|
|
+ * application with component composition. Shows how to:
|
|
|
|
|
+ * - Build a complete CRUD interface with Components
|
|
|
|
|
+ * - Use set_outlet_child/set_outlet_children for component composition
|
|
|
|
|
+ * - Handle form submissions
|
|
|
|
|
+ *
|
|
|
|
|
+ * This example mirrors the Astralis DocumentBuilder example but
|
|
|
|
|
+ * uses the Component class abstraction for better code organization.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Usage: todo-component [port]
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+// Simple task model for our todo list
|
|
|
|
|
+class TodoItem : Object {
|
|
|
|
|
+ public int id { get; set; }
|
|
|
|
|
+ public string title { get; set; }
|
|
|
|
|
+ public bool completed { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ public TodoItem(int id, string title, bool completed = false) {
|
|
|
|
|
+ this.id = id;
|
|
|
|
|
+ this.title = title;
|
|
|
|
|
+ this.completed = completed;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// In-memory todo store
|
|
|
|
|
+class TodoStore : Object {
|
|
|
|
|
+ private Series<TodoItem> items = new Series<TodoItem>();
|
|
|
|
|
+ private int next_id = 1;
|
|
|
|
|
+
|
|
|
|
|
+ public TodoStore() {
|
|
|
|
|
+ // Add some initial items
|
|
|
|
|
+ add("Learn Spry.Component");
|
|
|
|
|
+ add("Build dynamic HTML pages");
|
|
|
|
|
+ add("Handle form submissions");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void add(string title) {
|
|
|
|
|
+ items.add(new TodoItem(next_id++, title));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void toggle(int id) {
|
|
|
|
|
+ items.to_immutable_buffer().iterate((item) => {
|
|
|
|
|
+ if (item.id == id) {
|
|
|
|
|
+ item.completed = !item.completed;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void remove(int id) {
|
|
|
|
|
+ var new_items = items.to_immutable_buffer()
|
|
|
|
|
+ .where(item => item.id != id)
|
|
|
|
|
+ .to_series();
|
|
|
|
|
+ items = new_items;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public ImmutableBuffer<TodoItem> all() {
|
|
|
|
|
+ return items.to_immutable_buffer();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public int count() {
|
|
|
|
|
+ return (int)items.to_immutable_buffer().count();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public int completed_count() {
|
|
|
|
|
+ return (int)items.to_immutable_buffer()
|
|
|
|
|
+ .count(item => item.completed);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Main application with todo store
|
|
|
|
|
+TodoStore todo_store;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * EmptyListComponent - Shown when no items exist.
|
|
|
|
|
+ */
|
|
|
|
|
+class EmptyListComponent : Component {
|
|
|
|
|
+ public override string markup { get {
|
|
|
|
|
+ return """
|
|
|
|
|
+ <div class="empty">
|
|
|
|
|
+ No tasks yet! Add one below.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ """;
|
|
|
|
|
+ }}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * TodoItemComponent - A single todo item.
|
|
|
|
|
+ */
|
|
|
|
|
+class TodoItemComponent : Component {
|
|
|
|
|
+ public override string markup { get {
|
|
|
|
|
+ return """
|
|
|
|
|
+ <div class="todo-item">
|
|
|
|
|
+ <form method="POST" action="/toggle" style="display: inline;">
|
|
|
|
|
+ <input type="hidden" name="id" id="toggle-id"/>
|
|
|
|
|
+ <button type="submit" class="btn-toggle" id="toggle-btn"></button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ <span class="title" id="title"></span>
|
|
|
|
|
+ <form method="POST" action="/delete" style="display: inline;">
|
|
|
|
|
+ <input type="hidden" name="id" id="delete-id"/>
|
|
|
|
|
+ <button type="submit" class="btn-delete">Delete</button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ """;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ public int item_id { set {
|
|
|
|
|
+ instance.get_element_by_id("toggle-id").set_attribute("value", value.to_string());
|
|
|
|
|
+ instance.get_element_by_id("delete-id").set_attribute("value", value.to_string());
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ public string title { set {
|
|
|
|
|
+ instance.get_element_by_id("title").text_content = value;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ public bool completed { set {
|
|
|
|
|
+ var btn = instance.get_element_by_id("toggle-btn");
|
|
|
|
|
+ btn.text_content = value ? "Undo" : "Done";
|
|
|
|
|
+ }}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * TodoListComponent - Container for todo items.
|
|
|
|
|
+ */
|
|
|
|
|
+class TodoListComponent : Component {
|
|
|
|
|
+
|
|
|
|
|
+ public void set_items(Enumerable<Component> items) {
|
|
|
|
|
+ set_outlet_children("items", items);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public override string markup { get {
|
|
|
|
|
+ return """
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <h2>Todo List</h2>
|
|
|
|
|
+ <spry-outlet id="items"/>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ """;
|
|
|
|
|
+ }}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * HeaderComponent - Page header with stats.
|
|
|
|
|
+ */
|
|
|
|
|
+class HeaderComponent : Component {
|
|
|
|
|
+ public override string markup { get {
|
|
|
|
|
+ return """
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <h1>Todo Component Example</h1>
|
|
|
|
|
+ <p>This page demonstrates <code>Spry.Component</code> for building dynamic pages.</p>
|
|
|
|
|
+ <div class="stats">
|
|
|
|
|
+ <div class="stat">
|
|
|
|
|
+ <div class="stat-value" id="total"></div>
|
|
|
|
|
+ <div class="stat-label">Total Tasks</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat">
|
|
|
|
|
+ <div class="stat-value" id="completed"></div>
|
|
|
|
|
+ <div class="stat-label">Completed</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ """;
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ public int total_tasks { set {
|
|
|
|
|
+ instance.get_element_by_id("total").text_content = value.to_string();
|
|
|
|
|
+ }}
|
|
|
|
|
+
|
|
|
|
|
+ public int completed_tasks { set {
|
|
|
|
|
+ instance.get_element_by_id("completed").text_content = value.to_string();
|
|
|
|
|
+ }}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * AddFormComponent - Form to add new todos.
|
|
|
|
|
+ */
|
|
|
|
|
+class AddFormComponent : Component {
|
|
|
|
|
+ public override string markup { get {
|
|
|
|
|
+ return """
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <h2>Add New Task</h2>
|
|
|
|
|
+ <form method="POST" action="/add" style="display: flex; gap: 10px;">
|
|
|
|
|
+ <input type="text" name="title" placeholder="Enter a new task..." required="required"/>
|
|
|
|
|
+ <button type="submit" class="btn-primary">Add Task</button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ """;
|
|
|
|
|
+ }}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * FeaturesComponent - Shows Component features used.
|
|
|
|
|
+ */
|
|
|
|
|
+class FeaturesComponent : Component {
|
|
|
|
|
+ public override string markup { get {
|
|
|
|
|
+ return """
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <h2>Component Features Used</h2>
|
|
|
|
|
+ <div class="feature">
|
|
|
|
|
+ <strong>Defining Components:</strong>
|
|
|
|
|
+ <code>class MyComponent : Component { public override string markup { get { return "..."; } } }</code>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="feature">
|
|
|
|
|
+ <strong>Using Outlets:</strong>
|
|
|
|
|
+ <code><spry-outlet id="content"/></code>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="feature">
|
|
|
|
|
+ <strong>Setting Outlet Content:</strong>
|
|
|
|
|
+ <code>set_outlet_child("content", child);</code>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="feature">
|
|
|
|
|
+ <strong>Returning Results:</strong>
|
|
|
|
|
+ <code>return component.to_result();</code>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ """;
|
|
|
|
|
+ }}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * FooterComponent - Page footer.
|
|
|
|
|
+ */
|
|
|
|
|
+class FooterComponent : Component {
|
|
|
|
|
+ public override string markup { get {
|
|
|
|
|
+ return """
|
|
|
|
|
+ <div class="card" style="text-align: center; color: #666;">
|
|
|
|
|
+ <p>Built with <code>Spry.Component</code> |
|
|
|
|
|
+ <a href="/api/todos">View JSON API</a>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ """;
|
|
|
|
|
+ }}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * PageLayoutComponent - The main page structure.
|
|
|
|
|
+ */
|
|
|
|
|
+class PageLayoutComponent : Component {
|
|
|
|
|
+
|
|
|
|
|
+ public void set_header(Component component) {
|
|
|
|
|
+ set_outlet_child("header", component);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void set_todo_list(Component component) {
|
|
|
|
|
+ set_outlet_child("todo-list", component);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void set_add_form(Component component) {
|
|
|
|
|
+ set_outlet_child("add-form", component);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void set_features(Component component) {
|
|
|
|
|
+ set_outlet_child("features", component);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void set_footer(Component component) {
|
|
|
|
|
+ set_outlet_child("footer", component);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public override string markup { get {
|
|
|
|
|
+ return """
|
|
|
|
|
+ <!DOCTYPE html>
|
|
|
|
|
+ <html>
|
|
|
|
|
+ <head>
|
|
|
|
|
+ <meta charset="UTF-8"/>
|
|
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
|
|
|
+ <title>Todo Component Example</title>
|
|
|
|
|
+ <style>
|
|
|
|
|
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
|
|
|
|
+ .card { background: white; border-radius: 8px; padding: 20px; margin: 10px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
|
|
|
+ h1 { color: #333; margin-top: 0; }
|
|
|
|
|
+ h2 { color: #555; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }
|
|
|
|
|
+ .todo-item { display: flex; align-items: center; padding: 10px; margin: 5px 0; background: #fafafa; border-radius: 4px; border-left: 4px solid #4CAF50; }
|
|
|
|
|
+ .todo-item.completed { border-left-color: #ccc; opacity: 0.7; }
|
|
|
|
|
+ .todo-item.completed .title { text-decoration: line-through; color: #888; }
|
|
|
|
|
+ .todo-item .title { flex: 1; margin: 0 10px; }
|
|
|
|
|
+ .todo-item form { margin: 0; }
|
|
|
|
|
+ .stats { display: flex; gap: 20px; margin: 10px 0; }
|
|
|
|
|
+ .stat { background: #e8f5e9; padding: 10px 20px; border-radius: 4px; }
|
|
|
|
|
+ .stat-value { font-size: 24px; font-weight: bold; color: #4CAF50; }
|
|
|
|
|
+ .stat-label { font-size: 12px; color: #666; }
|
|
|
|
|
+ input[type="text"] { padding: 10px; border: 1px solid #ddd; border-radius: 4px; flex: 1; }
|
|
|
|
|
+ button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
|
|
|
|
|
+ .btn-primary { background: #4CAF50; color: white; }
|
|
|
|
|
+ .btn-primary:hover { background: #45a049; }
|
|
|
|
|
+ .btn-toggle { background: #2196F3; color: white; padding: 5px 10px; font-size: 12px; }
|
|
|
|
|
+ .btn-delete { background: #f44336; color: white; padding: 5px 10px; font-size: 12px; }
|
|
|
|
|
+ code { background: #e8e8e8; padding: 2px 6px; border-radius: 4px; font-size: 14px; }
|
|
|
|
|
+ pre { background: #263238; color: #aed581; padding: 15px; border-radius: 4px; overflow-x: auto; }
|
|
|
|
|
+ .feature { margin: 15px 0; }
|
|
|
|
|
+ .feature code { display: block; background: #f5f5f5; padding: 10px; margin: 5px 0; }
|
|
|
|
|
+ a { color: #2196F3; text-decoration: none; }
|
|
|
|
|
+ a:hover { text-decoration: underline; }
|
|
|
|
|
+ .empty { color: #999; font-style: italic; padding: 20px; text-align: center; }
|
|
|
|
|
+ </style>
|
|
|
|
|
+ </head>
|
|
|
|
|
+ <body>
|
|
|
|
|
+ <spry-outlet id="header"/>
|
|
|
|
|
+ <spry-outlet id="todo-list"/>
|
|
|
|
|
+ <spry-outlet id="add-form"/>
|
|
|
|
|
+ <spry-outlet id="features"/>
|
|
|
|
|
+ <spry-outlet id="footer"/>
|
|
|
|
|
+ </body>
|
|
|
|
|
+ </html>
|
|
|
|
|
+ """;
|
|
|
|
|
+ }}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Home page endpoint - builds the component tree
|
|
|
|
|
+class HomePageEndpoint : Object, Endpoint {
|
|
|
|
|
+ public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
|
|
|
|
|
+ // Create the page layout
|
|
|
|
|
+ var page = new PageLayoutComponent();
|
|
|
|
|
+
|
|
|
|
|
+ // Create header with stats
|
|
|
|
|
+ var header = new HeaderComponent();
|
|
|
|
|
+ header.total_tasks = todo_store.count();
|
|
|
|
|
+ header.completed_tasks = todo_store.completed_count();
|
|
|
|
|
+ page.set_header(header);
|
|
|
|
|
+
|
|
|
|
|
+ // Create todo list
|
|
|
|
|
+ var todo_list = new TodoListComponent();
|
|
|
|
|
+
|
|
|
|
|
+ var count = todo_store.count();
|
|
|
|
|
+ if (count == 0) {
|
|
|
|
|
+ todo_list.set_items(Iterate.single<Component>(new EmptyListComponent()));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ var items = new Series<Component>();
|
|
|
|
|
+ todo_store.all().iterate((item) => {
|
|
|
|
|
+ var component = new TodoItemComponent();
|
|
|
|
|
+ component.item_id = item.id;
|
|
|
|
|
+ component.title = item.title;
|
|
|
|
|
+ component.completed = item.completed;
|
|
|
|
|
+ items.add(component);
|
|
|
|
|
+ });
|
|
|
|
|
+ todo_list.set_items(items);
|
|
|
|
|
+ }
|
|
|
|
|
+ page.set_todo_list(todo_list);
|
|
|
|
|
+
|
|
|
|
|
+ // Add form
|
|
|
|
|
+ page.set_add_form(new AddFormComponent());
|
|
|
|
|
+
|
|
|
|
|
+ // Features
|
|
|
|
|
+ page.set_features(new FeaturesComponent());
|
|
|
|
|
+
|
|
|
|
|
+ // Footer
|
|
|
|
|
+ page.set_footer(new FooterComponent());
|
|
|
|
|
+
|
|
|
|
|
+ // to_result() handles all outlet replacement automatically
|
|
|
|
|
+ return page.to_result();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Add todo endpoint
|
|
|
|
|
+class AddTodoEndpoint : Object, Endpoint {
|
|
|
|
|
+ public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
|
|
|
|
|
+ // Parse form data
|
|
|
|
|
+ FormData form_data = yield FormDataParser.parse(
|
|
|
|
|
+ context.request.request_body,
|
|
|
|
|
+ context.request.content_type
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ var title = form_data.get_field("title");
|
|
|
|
|
+
|
|
|
|
|
+ if (title != null && title.strip() != "") {
|
|
|
|
|
+ todo_store.add(title.strip());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Redirect back to home (302 Found)
|
|
|
|
|
+ return new HttpStringResult("", (StatusCode)302)
|
|
|
|
|
+ .set_header("Location", "/");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Toggle todo endpoint
|
|
|
|
|
+class ToggleTodoEndpoint : Object, Endpoint {
|
|
|
|
|
+ public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
|
|
|
|
|
+ FormData form_data = yield FormDataParser.parse(
|
|
|
|
|
+ context.request.request_body,
|
|
|
|
|
+ context.request.content_type
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ var id_str = form_data.get_field("id");
|
|
|
|
|
+ if (id_str != null) {
|
|
|
|
|
+ var id = int.parse(id_str);
|
|
|
|
|
+ todo_store.toggle(id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return new HttpStringResult("", (StatusCode)302)
|
|
|
|
|
+ .set_header("Location", "/");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Delete todo endpoint
|
|
|
|
|
+class DeleteTodoEndpoint : Object, Endpoint {
|
|
|
|
|
+ public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
|
|
|
|
|
+ FormData form_data = yield FormDataParser.parse(
|
|
|
|
|
+ context.request.request_body,
|
|
|
|
|
+ context.request.content_type
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ var id_str = form_data.get_field("id");
|
|
|
|
|
+ if (id_str != null) {
|
|
|
|
|
+ var id = int.parse(id_str);
|
|
|
|
|
+ todo_store.remove(id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return new HttpStringResult("", (StatusCode)302)
|
|
|
|
|
+ .set_header("Location", "/");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// API endpoint that returns JSON representation of todos
|
|
|
|
|
+class TodoJsonEndpoint : Object, Endpoint {
|
|
|
|
|
+ public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
|
|
|
|
|
+ var json_parts = new Series<string>();
|
|
|
|
|
+ json_parts.add("{\"todos\": [");
|
|
|
|
|
+
|
|
|
|
|
+ bool first = true;
|
|
|
|
|
+ todo_store.all().iterate((item) => {
|
|
|
|
|
+ if (!first) json_parts.add(",");
|
|
|
|
|
+ var completed_str = item.completed ? "true" : "false";
|
|
|
|
|
+ var escaped_title = item.title.replace("\"", "\\\"");
|
|
|
|
|
+ json_parts.add(@"{\"id\":$(item.id),\"title\":\"$escaped_title\",\"completed\":$completed_str}");
|
|
|
|
|
+ first = false;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ json_parts.add("]}");
|
|
|
|
|
+
|
|
|
|
|
+ var json = json_parts.to_immutable_buffer()
|
|
|
|
|
+ .aggregate<string>("", (acc, s) => acc + s);
|
|
|
|
|
+
|
|
|
|
|
+ return new HttpStringResult(json)
|
|
|
|
|
+ .set_header("Content-Type", "application/json");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void main(string[] args) {
|
|
|
|
|
+ int port = args.length > 1 ? int.parse(args[1]) : 8080;
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize the todo store
|
|
|
|
|
+ todo_store = new TodoStore();
|
|
|
|
|
+
|
|
|
|
|
+ print("═══════════════════════════════════════════════════════════════\n");
|
|
|
|
|
+ print(" Spry TodoComponent Example\n");
|
|
|
|
|
+ print("═══════════════════════════════════════════════════════════════\n");
|
|
|
|
|
+ print(" Port: %d\n", port);
|
|
|
|
|
+ print("═══════════════════════════════════════════════════════════════\n");
|
|
|
|
|
+ print(" Endpoints:\n");
|
|
|
|
|
+ print(" / - Todo list (Component-based)\n");
|
|
|
|
|
+ print(" /add - Add a todo item (POST)\n");
|
|
|
|
|
+ print(" /toggle - Toggle todo completion (POST)\n");
|
|
|
|
|
+ print(" /delete - Delete a todo item (POST)\n");
|
|
|
|
|
+ print(" /api/todos - JSON API for todos\n");
|
|
|
|
|
+ print("═══════════════════════════════════════════════════════════════\n");
|
|
|
|
|
+ print("\nPress Ctrl+C to stop the server\n\n");
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ var application = new WebApplication(port);
|
|
|
|
|
+
|
|
|
|
|
+ // Register compression components (optional, for better performance)
|
|
|
|
|
+ application.use_compression();
|
|
|
|
|
+
|
|
|
|
|
+ // Register endpoints
|
|
|
|
|
+ application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
|
|
|
|
|
+ application.add_endpoint<AddTodoEndpoint>(new EndpointRoute("/add"));
|
|
|
|
|
+ application.add_endpoint<ToggleTodoEndpoint>(new EndpointRoute("/toggle"));
|
|
|
|
|
+ application.add_endpoint<DeleteTodoEndpoint>(new EndpointRoute("/delete"));
|
|
|
|
|
+ application.add_endpoint<TodoJsonEndpoint>(new EndpointRoute("/api/todos"));
|
|
|
|
|
+
|
|
|
|
|
+ application.run();
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ printerr("Error: %s\n", e.message);
|
|
|
|
|
+ Process.exit(1);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|