Simple Todo App Project with Golang and MongoDB

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.Database
const (
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)
}
}
<!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 }">&nbsp;</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>

--

--

--

Software Engineer — Openshift CoreOS

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Develop a Full-Fledged Component Library with React, just like Material UI

SocketIO NodeJS Integration Testing

Useful Chrome Developer Tools for Beginners in Web Development

Deploying a React App to Firebase made Easy.

Building a Node app that uploads data to Google Drive

Luckily, the StatusBar module from React Native also exports imperative means of setting this…

30 JavaScript Libraries That You Don’t Use But Should

What It is Like to Work with a Good Software Developer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Ayesha Kaleem

Ayesha Kaleem

Software Engineer — Openshift CoreOS

More from Medium

Battle of Concurrency | Goroutines vs Threads.

Learning Go

json: cannot unmarshal object into a Go struct field

Building a Yahoo Finance Scraper in Go, saving to CSV and optionally sending yourself an email