Simple Todo App Project with Golang and MongoDB
main.go
package mainimport (
"context"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"strings"
"time""github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/thedevsaddam/renderer"
mgo "gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)var rnd *renderer.Render
var db *mgo.Databaseconst (
hostName string = "localhost:27017"
dbName string = "demo_todo"
collectionName string = "todo"
port string = ":9000"
)type (
todoModel struct {
ID bson.ObjectId `bson:"_id,omitempty"`
Title string `bson:"title"`
Completed bool `bson:"completed"`
CreatedAt time.Time `bson:"createdAt"`
}
todo struct {
ID string `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
CreatedAt time.Time `json:"created_at"`
}
)func init() {
rnd = renderer.New()
sess, err := mgo.Dial(hostName)
checkErr(err)
sess.SetMode(mgo.Monotonic, true)
db = sess.DB(dbName)
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
err := rnd.Template(w, http.StatusOK, []string{"static/home.tpl"}, nil)
checkErr(err)
}
func fetchTodos(w http.ResponseWriter, r *http.Request) {
todos := []todoModel{}
if err := db.C(collectionName).Find(bson.M{}).All(&todos); err != nil {
rnd.JSON(w, http.StatusProcessing, renderer.M{
"message": "Failed to fetch todo",
"error": err,
})
return
}
todoList := []todo{}
for _, t := range todos {
todoList = append(todoList, todo{
ID: t.ID.Hex(),
Title: t.Title,
Completed: t.Completed,
CreatedAt: t.CreatedAt,
})
}
rnd.JSON(w, http.StatusOK, renderer.M{
"data": todoList,
})
}
func createTodo(w http.ResponseWriter, r *http.Request) {
var t todo
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
rnd.JSON(w, http.StatusProcessing, err)
return
}
if t.Title == "" {
rnd.JSON(w, http.StatusBadRequest, renderer.M{
"message": "The title is required",
})
return
}
tm := todoModel{
ID: bson.NewObjectId(),
Title: t.Title,
Completed: false,
CreatedAt: time.Now(),
}
if err := db.C(collectionName).Insert(&tm); err != nil {
rnd.JSON(w, http.StatusProcessing, renderer.M{
"message": "Failed to save todo",
"error": err,
})
return
}
rnd.JSON(w, http.StatusCreated, renderer.M{
"message": "todo created successfully",
"todo_id": tm.ID.Hex(),
})
}
func deleteTodo(w http.ResponseWriter, r *http.Request) {
id := strings.TrimSpace(chi.URLParam(r, "id"))if !bson.IsObjectIdHex(id) {
rnd.JSON(w, http.StatusBadRequest, renderer.M{
"message": "The ID is invalid",
})
return
}
if err := db.C(collectionName).RemoveId(bson.ObjectIdHex(id)); err != nil {
rnd.JSON(w, http.StatusProcessing, renderer.M{
"message": "failed to delete todo",
"error": err,
})
return
}
rnd.JSON(w, http.StatusOK, renderer.M{
"message": "todo deleted successfully",
})
}
func updateTodo(w http.ResponseWriter, r *http.Request) {
id := strings.TrimSpace(chi.URLParam(r, "id"))
if !bson.IsObjectIdHex(id) {
rnd.JSON(w, http.StatusBadRequest, renderer.M{
"message": "The id is invalid",
})
return
}
var t todo
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
rnd.JSON(w, http.StatusProcessing, err)
return
}
if t.Title == "" {
rnd.JSON(w, http.StatusBadRequest, renderer.M{
"message": "the title field is required",
})
return
}
if err := db.C(collectionName).Update(bson.M{"_id": bson.ObjectIdHex(id)}, bson.M{"title": t.Title, "completed": t.Completed}); err != nil {
rnd.JSON(w, http.StatusProcessing, renderer.M{
"message": "failed to update todo",
"error": err,
})
return
}}
func main() {
stopChan := make(chan os.Signal, 1)
signal.Notify(stopChan, os.Interrupt)r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/", homeHandler)
r.Mount("/todo", todoHandlers())srv := &http.Server{
Addr: port,
Handler: r,
// timeouts for databases
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
log.Println("Listening on port, port")
if err := srv.ListenAndServe(); err != nil {
log.Printf("listen:%s\n", err)
}
}()<-stopChan
log.Println("shutting down the server....")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
srv.Shutdown(ctx)
defer cancel()
log.Println("Server gracefully stopped!")}
func todoHandlers() http.Handler {
rg := chi.NewRouter()
rg.Group(func(r chi.Router) {
r.Get("/", fetchTodos)
r.Get("/", createTodo)
r.Put("/{id}", updateTodo)
r.Delete("/{id}", deleteTodo)
})
return rg
}
func checkErr(err error) {
if err != nil {
log.Fatal(err)
}
}
static/home.tpl
<!doctype html>
<html lang="en">
<head>
<title>Todo</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script type="text/javascript" src="https://unpkg.com/vue@2.3.4"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-resource@1.3.4"></script>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<style type="text/css">
.del {
text-decoration: line-through;
}
.card{
border-radius: 0 !important;
border: none;
}
.card-body{
padding: 0 !important;
}
.todo-title{
width: 100%;
background: #b88f92;
color: #FFF
;
font-size: 30px;
font-weight: bold;
padding: 20px 10px;
text-align: center;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.custom-input{
border-radius: 0 !important;
padding: 10px 10px !important;
border-bottom: none;
}
.custom-input:focus, .custom-input:active{
box-shadow: none !important;
}
.custom-button{
border-radius: 0 !important;
cursor: pointer;
}
.custom-button:focus, .custom-button:active{
box-shadow: none !important;
}
.list-group li{
cursor: pointer;
border-radius: 0 !important;
}
.checked{
background: #5e6669;
color: #95a5a6;
}
.error{
border: 2px solid #e74c3c !important;
}
.not-checked{
background: #2227c7;
color: #FFF;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container" id="root">
<div class="row">
<div class="col-6 offset-3">
<br><br>
<div class="card">
<div class="todo-title">
Daily Todo Lists
</div>
<div class="card-body">
<form v-on:submit.prevent>
<div class="input-group">
<input type="text" v-model="todo.title" v-on:keyup="checkForEnter($event)" class="form-control custom-input" :class="{ 'error': showError }" placeholder="Add your todo">
<span class="input-group-btn">
<button class="btn custom-button" :class="{'btn-success' : !enableEdit, 'btn-warning' : enableEdit}" type="button" v-on:click="addTodo"><span :class="{'fa fa-plus' : !enableEdit, 'fa fa-edit' : enableEdit}"></span></button>
</span>
</div>
</form>
<ul class="list-group">
<li class="list-group-item" :class="{ 'checked': todo.completed, 'not-checked': !todo.completed }" v-for="(todo, todoIndex) in todos" v-on:click="toggleTodo(todo, todoIndex)">
<i :class="{'fa fa-circle': !todo.completed, 'fa fa-check-circle text-success': todo.completed }"> </i>
<span :class="{ 'del': todo.completed }">@{ todo.title }</span>
<div class="btn-group float-right" role="group" aria-label="Basic example">
<button type="button" class="btn btn-success btn-sm custom-button" v-on:click.prevent.stop v-on:click="editTodo(todo, todoIndex)"><span class="fa fa-edit"></span></button>
<button type="button" class="btn btn-danger btn-sm custom-button" v-on:click.prevent.stop v-on:click="deleteTodo(todo, todoIndex)"><span class="fa fa-trash"></span></button>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script>
<script type="text/javascript">
var Vue = new Vue({
el: '#root',
delimiters: ['@{', '}'],
data: {
showError: false,
enableEdit: false,
todo: {id: '', title: '', completed: false},
todos: []
},
mounted () {
this.$http.get('todo').then(response => {
this.todos = response.body.data;
});
},
methods: {
addTodo(){
if (this.todo.title == ''){
this.showError = true;
}else{
this.showError = false;
if(this.enableEdit){
this.$http.put('todo/'+this.todo.id, this.todo).then(response => {
if(response.status == 200){
this.todos[this.todo.todoIndex] = this.todo;
}
});
this.todo = {id: '', title: '', completed: false};
this.enableEdit = false;
}else{
this.$http.post('todo', {title: this.todo.title}).then(response => {
if(response.status == 201){
this.todos.push({id: response.body.todo_id, title: this.todo.title, completed: false});
this.todo = {id: '', title: '', completed: false};
}
});
}
}
},
checkForEnter(event){
if (event.key == "Enter") {
this.addTodo();
}
},
toggleTodo(todo, todoIndex){
var completedToggle;
if (todo.completed == true) {
completedToggle = false;
}else{
completedToggle = true;
}
this.$http.put('todo/'+todo.id, {id: todo.id, title: todo.title, completed: completedToggle}).then(response => {
if(response.status == 200){
this.todos[todoIndex].completed = completedToggle;
}
});
},
editTodo(todo, todoIndex){
this.enableEdit = true;
this.todo = todo;
this.todo.todoIndex = todoIndex;
},
deleteTodo(todo, todoIndex){
if(confirm("Are you sure ?")){
this.$http.delete('todo/'+todo.id).then(response => {
if(response.status == 200){
this.todos.splice(todoIndex, 1);
this.todo = {id: '', title: '', completed: false};
}
});
}
}
}
});
</script>
</body>
</html>
Stay tuned for more fun projects! 😍