Skip to content

Commit

Permalink
examples: add TODO app example with x.vweb (#20175)
Browse files Browse the repository at this point in the history
  • Loading branch information
Casper64 committed Dec 14, 2023
1 parent 22cb9c5 commit 70c575a
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 7 deletions.
1 change: 1 addition & 0 deletions examples/xvweb/todo/.gitignore
@@ -0,0 +1 @@
*.db
19 changes: 19 additions & 0 deletions examples/xvweb/todo/README.md
@@ -0,0 +1,19 @@
# A simple TODO app using x.vweb

A simple TODO app using `x.vweb` showcasing how to build a basic web app with vweb.

## Database

This example uses an sqlite database using the `db.sqlite` package,
but you can use any database from the `db` module.

## Quick Start

Run from this directory with
```bash
v run main.v
```
You can also enable vweb's livereload feature with
```bash
v watch -d vweb_livereload run main.v`
```
122 changes: 122 additions & 0 deletions examples/xvweb/todo/assets/main.css
@@ -0,0 +1,122 @@

html, body {
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
margin: 0;
}

body {
max-width: 900px;
padding: 50px;
margin: auto;
}

* {
box-sizing: border-box;
}

/* simple styles reset */
button {
appearance: none;
outline: 0;
border: 0;
margin: 0;
padding: 0;
}

input {
appearance: none;
outline: 0;
font-size: 16px;
height: 30px;
line-height: 30px;
border: 1px solid #d3d3d3;
border-radius: 5px;
}

button {
font-size: 14px;
height: 30px;
padding: 5px 20px;
border-radius: 5px;
cursor: pointer;
}

button.primary {
background-color: #3b71ca;
color: white;
}
button.error {
background-color: red;
color: white;
}
button.success {
background-color: green;
color: white;
}

.form-success {
color: green;
}

.form-error {
color: red;
}

h1 {
text-align: center;
}

section {
padding: 20px;
}
section.create-todo {
max-width: fit-content;
margin: auto;
}

.todo-list {
display: flex;
flex-direction: column;
}

.todo {
display: flex;
align-items: center;
gap: 10px;
padding: 20px;
border-top: 1px solid #d3d3d3;
}

.todo .name {
flex-grow: 1;
font-weight: bold;
}

.todo-id, .time {
font-size: 14px;
font-weight: normal;
color: #3d3d3d;
margin: 0px 10px;
}

/* we're mobile friendly */
@media only screen and (max-width: 900px) {
body {
max-width: unset;
}

.todo {
flex-direction: column;
gap: 5px;
}
.todo p {
margin: 0;
}

section.create-todo form {
display: flex;
flex-direction: column;
gap: 10px;
}
}
145 changes: 145 additions & 0 deletions examples/xvweb/todo/main.v
@@ -0,0 +1,145 @@
// Simple TODO app using x.vweb
// Run from this directory with `v run main.v`
// You can also enable vwebs livereload feature with
// `v watch -d vweb_livereload run main.v`
module main

import x.vweb
import db.sqlite
import time

struct Todo {
pub mut:
// `id` is the primary field. The attribute `sql: serial` acts like AUTO INCREMENT in sql.
// You can use this attribute if you want a unique id for each row.
id int @[primary; sql: serial]
name string
completed bool
created time.Time
updated time.Time
}

pub struct Context {
vweb.Context
pub mut:
// we can use this field to check whether we just created a TODO in our html templates
created_todo bool
}

pub struct App {
vweb.StaticHandler
pub:
// we can access the SQLITE database directly via `app.db`
db sqlite.DB
}

// This method will only handle GET requests to the index page
@[get]
pub fn (app &App) index(mut ctx Context) vweb.Result {
todos := sql app.db {
select from Todo
} or { return ctx.server_error('could not fetch todos from database!') }

// TODO: use $vweb.html()
return ctx.html($tmpl('templates/index.html'))
}

// This method will only handle POST requests to the index page
@['/'; post]
pub fn (app &App) create_todo(mut ctx Context, name string) vweb.Result {
// We can receive form input fields as arguments in a route!
// we could also access the name field by doing `name := ctx.form['name']`

// validate input field
if name.len == 0 {
// set a form error
ctx.form_error = 'You must fill in all the fields!'
// send a HTTP 400 response code indicating that the form fields are incorrect
ctx.res.set_status(.bad_request)
// render the home page
return app.index(mut ctx)
}

// create a new todo
todo := Todo{
name: name
created: time.now()
updated: time.now()
}

// insert the todo into our database
sql app.db {
insert todo into Todo
} or { return ctx.server_error('could not insert a new TODO in the datbase') }

ctx.created_todo = true

// render the home page
return app.index(mut ctx)
}

@['/todo/:id/complete'; post]
pub fn (app &App) complete_todo(mut ctx Context, id int) vweb.Result {
// first check if there exist a TODO record with `id`
todos := sql app.db {
select from Todo where id == id
} or { return ctx.server_error("could not fetch TODO's") }
if todos.len == 0 {
// return HTTP 404 when the TODO does not exist
ctx.res.set_status(.not_found)
return ctx.text('There is no TODO item with id=${id}')
}

// update the TODO field
sql app.db {
update Todo set completed = true, updated = time.now() where id == id
} or { return ctx.server_error('could not update TODO') }

// redirect client to the home page and tell the browser to sent a GET request
return ctx.redirect('/', .see_other)
}

@['/todo/:id/delete'; post]
pub fn (app &App) delete_todo(mut ctx Context, id int) vweb.Result {
// first check if there exist a TODO record with `id`
todos := sql app.db {
select from Todo where id == id
} or { return ctx.server_error("could not fetch TODO's") }
if todos.len == 0 {
// return HTTP 404 when the TODO does not exist
ctx.res.set_status(.not_found)
return ctx.text('There is no TODO item with id=${id}')
}

// prevent hackers from deleting TODO's that are not completed ;)
to_be_deleted := todos[0]
if to_be_deleted.completed == false {
return ctx.request_error('You must first complete a TODO before you can delete it!')
}

// delete the todo
sql app.db {
delete from Todo where id == id
} or { return ctx.server_error('could not delete TODO') }

// redirect client to the home page and tell the browser to sent a GET request
return ctx.redirect('/', .see_other)
}

fn main() {
// create a new App instance with a connection to the datbase
mut app := &App{
db: sqlite.connect('todo.db')!
}

// mount the assets folder at `/assets/`
app.handle_static('assets', false)!

// create the table in our database, if it doesn't exist
sql app.db {
create table Todo
}!

// start our app at port 8080
vweb.run[App, Context](mut app, 8080)
}
64 changes: 64 additions & 0 deletions examples/xvweb/todo/templates/index.html
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My TODO App</title>
<!-- include our css from the assets folder -->
@css '/assets/main.css'
</head>
<body>

<header>
<h1>List of all my todos</h1>
</header>

<main>
<!-- Display a message when a new TODO is created -->
@if ctx.created_todo
<p class="form-success">Created a new todo!</p>
@endif

<section class="todos">
<div class="todo-list">
@if todos.len == 0
<p>Nothing to see here...</p>
@endif
<!-- Loop over all the current todo's -->
@for todo in todos
<div class="todo">
<p class="name"><span class="todo-id">(id: @{todo.id})</span>@{todo.name}</p>
@if !todo.completed
<!-- We can also call methods of properties inside a template -->
<p class="time">Created at: <span class="time">@{todo.created.hhmmss()}</span></p>
<!-- Pass the id of the TODO as a route parameter to '/complete/:id' -->
<form action="/todo/@{todo.id}/complete" method="post">
<button class="success" type="submit">Complete</button>
</form>
@else
<p class="time">Completed at: <span class="time">@{todo.updated.hhmmss()}</span></p>
<p class="completed">✔️</p>
<!-- Pass the id of the TODO as a route parameter to '/complete/:id' -->
<form action="/todo/@{todo.id}/delete" method="post">
<button class="error" type="submit">Delete</button>
</form>
@endif
</div>
@endfor

</div>
</section>

<section class="create-todo">
<h2>Create a new TODO item</h2>
<form action="/" method="post">
<label for="task-name">Name:</label>
<input autofocus id="task-name" type="text" name="name">
<button class="primary" type="submit">Create</button>
<p class="form-error">@{ctx.form_error}</p>
</form>
</section>
</main>

</body>
</html>

0 comments on commit 70c575a

Please sign in to comment.