Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
examples: add TODO app example with x.vweb (#20175)
- Loading branch information
Showing
8 changed files
with
383 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
*.db |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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` | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
Oops, something went wrong.