├── .gitignore ├── .env ├── static ├── assets │ ├── edit-24.png │ ├── enter-24.png │ ├── trash-24.png │ ├── replace-24.png │ └── three-dot-24.png ├── index.js ├── style.css └── app.js ├── models ├── templateData.go └── models.go ├── middleware └── middleware.go ├── main.go ├── go.mod ├── helpers └── helpers.go ├── routes └── routes.go ├── LICENSE ├── views └── index.html ├── README.md ├── go.sum └── controllers └── controllers.go /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | MONGO_URI = mongodb://127.0.0.1:27017/ 2 | DB_NAME = todo_list 3 | COLLECTION_NAME = todo -------------------------------------------------------------------------------- /static/assets/edit-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raihan2bd/go-todo-with-mongodb/HEAD/static/assets/edit-24.png -------------------------------------------------------------------------------- /static/assets/enter-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raihan2bd/go-todo-with-mongodb/HEAD/static/assets/enter-24.png -------------------------------------------------------------------------------- /static/assets/trash-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raihan2bd/go-todo-with-mongodb/HEAD/static/assets/trash-24.png -------------------------------------------------------------------------------- /static/assets/replace-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raihan2bd/go-todo-with-mongodb/HEAD/static/assets/replace-24.png -------------------------------------------------------------------------------- /models/templateData.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type TemplateData struct { 4 | CSRFToken string 5 | Todos []Todo 6 | } 7 | -------------------------------------------------------------------------------- /static/assets/three-dot-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raihan2bd/go-todo-with-mongodb/HEAD/static/assets/three-dot-24.png -------------------------------------------------------------------------------- /middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/justinas/nosurf" 7 | ) 8 | 9 | // Nosurf CSRF protection to all POST request 10 | func Nosurf(next http.Handler) http.Handler { 11 | csrfHandler := nosurf.New(next) 12 | 13 | csrfHandler.SetBaseCookie(http.Cookie{ 14 | HttpOnly: true, 15 | Path: "/", 16 | Secure: true, 17 | SameSite: http.SameSiteLaxMode, 18 | }) 19 | 20 | return csrfHandler 21 | } 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | router "github.com/raihan2bd/go-todo-with-mongodb/routes" 9 | ) 10 | 11 | func main() { 12 | 13 | srv := &http.Server{ 14 | Addr: ":9000", 15 | Handler: router.Router(), 16 | ReadTimeout: 60 * time.Second, 17 | WriteTimeout: 60 * time.Second, 18 | IdleTimeout: 60 * time.Second, 19 | } 20 | 21 | log.Println("Server is running on http://localhost:9000") 22 | log.Fatal(srv.ListenAndServe()) 23 | } 24 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | type TodoModel struct { 10 | ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` 11 | Title string `json:"title" bson:"title"` 12 | Completed bool `json:"completed" bson:"completed"` 13 | CreatedAt time.Time `json:"created_at" bson:"createdAt"` 14 | } 15 | 16 | type Todo struct { 17 | ID string `json:"id" bson:"_id,omitempty"` 18 | Title string `json:"title" bson:"title"` 19 | Completed bool `json:"completed" bson:"completed"` 20 | CreatedAt time.Time `json:"created_at" bson:"createdAt"` 21 | } 22 | -------------------------------------------------------------------------------- /static/index.js: -------------------------------------------------------------------------------- 1 | // Select dom element 2 | const showMsg = document.querySelector(".show-msg"); 3 | const btnSubmit = document.querySelector(".btn-submit"); 4 | const formInput = document.getElementById("description"); 5 | const todoItemsDes = document.querySelectorAll(".todo-des"); 6 | const filterButton = document.querySelector(".btn-clear-all"); 7 | const todoContainer = document.querySelector(".todo-items-gropu"); 8 | 9 | const todoData = []; 10 | 11 | const fetchTodos = async () => { 12 | const res = await fetch("todo/"); 13 | 14 | if (!res.ok) { 15 | console.log(res); 16 | } 17 | 18 | const result = await res.json(); 19 | console.log(result); 20 | }; 21 | 22 | window.onload = () => { 23 | // fetchTodos(); 24 | console.log("hi"); 25 | }; 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/raihan2bd/go-todo-with-mongodb 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/go-chi/chi/v5 v5.0.8 7 | github.com/joho/godotenv v1.4.0 8 | github.com/justinas/nosurf v1.1.1 9 | github.com/thedevsaddam/renderer v1.2.0 10 | go.mongodb.org/mongo-driver v1.11.1 11 | ) 12 | 13 | require ( 14 | github.com/golang/snappy v0.0.1 // indirect 15 | github.com/klauspost/compress v1.13.6 // indirect 16 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect 17 | github.com/pkg/errors v0.9.1 // indirect 18 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 19 | github.com/xdg-go/scram v1.1.1 // indirect 20 | github.com/xdg-go/stringprep v1.0.3 // indirect 21 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect 22 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect 23 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 24 | golang.org/x/text v0.3.7 // indirect 25 | gopkg.in/yaml.v2 v2.4.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/raihan2bd/go-todo-with-mongodb/models" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/mongo" 10 | ) 11 | 12 | var ctx = context.TODO() 13 | 14 | // Fetch todos form database 15 | func FetchTodosFormDB(db *mongo.Collection) ([]models.Todo, error) { 16 | // var todoM Todo 17 | var todos []models.TodoModel 18 | todoList := []models.Todo{} 19 | cur, err := db.Find(ctx, bson.D{}) 20 | if err != nil { 21 | defer cur.Close(ctx) 22 | return todoList, errors.New("failed to fetch todo") 23 | } 24 | 25 | if err = cur.All(ctx, &todos); err != nil { 26 | return todoList, errors.New("failed to load data") 27 | } 28 | 29 | for _, t := range todos { 30 | todoList = append(todoList, models.Todo{ 31 | ID: t.ID.Hex(), 32 | Title: t.Title, 33 | Completed: t.Completed, 34 | CreatedAt: t.CreatedAt, 35 | }) 36 | } 37 | 38 | return todoList, nil 39 | } 40 | -------------------------------------------------------------------------------- /routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi/v5" 7 | "github.com/raihan2bd/go-todo-with-mongodb/controllers" 8 | "github.com/raihan2bd/go-todo-with-mongodb/middleware" 9 | ) 10 | 11 | func Router() http.Handler { 12 | router := chi.NewRouter() 13 | router.Use(middleware.Nosurf) 14 | router.Get("/", controllers.HomeHandler) 15 | router.Mount("/todo", todoHandler()) 16 | router.Delete("/todo/delete-completed", controllers.DeleteCompleted) 17 | //serve static files 18 | fileServer := http.FileServer(http.Dir("./static/")) 19 | router.Handle("/static/*", http.StripPrefix("/static", fileServer)) 20 | 21 | return router 22 | } 23 | 24 | func todoHandler() http.Handler { 25 | rg := chi.NewRouter() 26 | rg.Group(func(r chi.Router) { 27 | r.Get("/", controllers.FetchTodos) 28 | r.Post("/", controllers.CreateTodo) 29 | r.Put("/{id}", controllers.UpdateTodo) 30 | r.Delete("/{id}", controllers.DeleteOneTodo) 31 | }) 32 | 33 | return rg 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Abu Raihan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | Go Todo List With MongoDB 15 | 16 | 17 |
18 | 19 |
20 |

Today's Todo

21 | 22 |
23 | 24 | 35 | 38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Todo List with Mongodb 2 |

Todo list project is a basic full-stack single page project. I am using Html, CSS, JavaScript for front-end and GO(golang) for backend and also using MongoDB(NoSql) for database. This project can save, update, delete todo and also delete completed todos. The main goal that How to connect Go and MongoDB and make a crud operation using them.

3 | 4 | ## Demo 5 | ![Capture](https://user-images.githubusercontent.com/35267447/209561297-b05cae4b-41ce-42e7-b91a-149e0f211eb6.PNG) 6 | 7 | ## 💻 Getting Started 8 | - To get star with this package first of all you have to clone the project ⬇️ 9 | ``` bash 10 | https://github.com/raihan2bd/go-todo-with-mongodb.git 11 | ``` 12 | - Then Make sure you have install [Go (golang)](https://go.dev/dl/) version 1.1.0 or latest stable version. 13 | - Then make sure you have install [Mongodb](https://www.mongodb.com/try/download/community) on your local mechine if you want to use this project as localy. 14 | - To install all the Go packages navigate the folder address on your terminal and enter the below command ⬇️ 15 | ``` bash 16 | go get ./... 17 | ``` 18 | - After downloading the packages you should edit .env file and change **MONGO_URI** to your own MongoDB hosted link or Mongodb Atlas link. and also change you can chage the other variables to your own Database name and also the Collection name. 19 | 20 | # Usages 21 | > *Note: Before enter the below command make sure you are in the right directory.* 22 | 23 | - To build the project as a single executable just run the below command. ⬇️ 24 | ``` bash 25 | go build 26 | ``` 27 | - After finishing the avove instructions you can see the project in your local mechine by entering the below command ⬇️ 28 | ```bash 29 | go run main.go 30 | ``` 31 | 32 | - Then you can see this project live on your browser by this link http://localhost:9000 or your given the port nuber you set for the project. 33 | 34 | 35 | ## 👥 Author 36 | 37 | 👤 **Abu Raihan** 38 | 39 | - GitHub: [@githubhandle](https://github.com/raihan2bd) 40 | - Twitter: [@twitterhandle](https://twitter.com/raihan2bd) 41 | - LinkedIn: [LinkedIn](https://linkedin.com/in/raihan2bd) 42 | 43 | 44 | ## ⭐️ Show your support 45 | 46 | > Thanks for visiting my repository. Give a ⭐️ if you like this project! 47 | 48 | ## 📝 License 49 | 50 | This project is [MIT](./LICENSE) licensed. 51 | 52 | ## Contribution 53 | *Your suggestions will be more than appreciated. If you want to suggest anything for this project feel free to do that. :slightly_smiling_face:* 54 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | overflow-x: hidden; 10 | font-family: 'Lato', sans-serif; 11 | } 12 | 13 | .df { 14 | display: flex; 15 | } 16 | 17 | .dn { 18 | display: none; 19 | } 20 | 21 | .db { 22 | display: block; 23 | } 24 | 25 | /* Todo Section start from here */ 26 | .container { 27 | background: #470255; 28 | color: #fff; 29 | display: flex; 30 | flex-direction: column; 31 | justify-content: center; 32 | align-items: center; 33 | min-height: 100vh; 34 | padding: 10px; 35 | } 36 | 37 | .todo-section { 38 | gap: 15px; 39 | width: 100%; 40 | margin: 20px 0; 41 | display: flex; 42 | max-width: 500px; 43 | align-items: center; 44 | flex-direction: column; 45 | border: 1px solid #fff; 46 | border-radius: 15px 15px 0 0; 47 | box-shadow: 0 5px 10px #1e0129; 48 | animation: slide-in 0.3s ease-in-out; 49 | } 50 | 51 | @keyframes slide-in { 52 | 0% { 53 | transform: translateY(-200%); 54 | opacity: 0.5; 55 | } 56 | 57 | 100% { 58 | transform: translateY(0%); 59 | opacity: 1; 60 | } 61 | } 62 | 63 | .todo-title { 64 | margin: 15px; 65 | font-size: 1.7rem; 66 | } 67 | 68 | .todo-title::after { 69 | content: ''; 70 | width: 50px; 71 | display: block; 72 | margin: 10px auto; 73 | border-bottom: 2px solid #470255; 74 | } 75 | 76 | /* Add new todo form start form here */ 77 | #todo-form { 78 | min-width: 100%; 79 | position: relative; 80 | margin-bottom: 10px; 81 | } 82 | 83 | .input-group { 84 | min-width: 100%; 85 | padding: 12px 15px; 86 | border: 1px solid transparent; 87 | } 88 | 89 | .input-group:focus-visible { 90 | outline-color: #a60ae4; 91 | } 92 | 93 | .invalid { 94 | color: #f55; 95 | border-color: #f55; 96 | } 97 | 98 | .invalid:focus-visible { 99 | outline-color: #f55; 100 | } 101 | 102 | .btn-submit { 103 | position: absolute; 104 | display: block; 105 | right: 15px; 106 | top: 8px; 107 | border: none; 108 | background: transparent; 109 | } 110 | 111 | .btn-submit img { 112 | width: 100%; 113 | } 114 | 115 | .btn-submit:active { 116 | transform: translateY(-2px); 117 | } 118 | 119 | /* Todo Items styles start from here */ 120 | .todo-items-gropu { 121 | list-style: none; 122 | min-width: 100%; 123 | } 124 | 125 | .todo-item { 126 | width: 100%; 127 | display: flex; 128 | padding: 15px; 129 | column-gap: 10px; 130 | align-items: center; 131 | cursor: normal; 132 | } 133 | 134 | .todo-item:nth-child(odd) { 135 | background: #7a0492; 136 | } 137 | 138 | .done { 139 | text-decoration: line-through; 140 | color: #de8cff !important; 141 | background: #6b407c !important; 142 | text-decoration: line-through; 143 | } 144 | 145 | #todo-compleate { 146 | cursor: pointer; 147 | } 148 | 149 | .todo-des { 150 | flex: 1; 151 | } 152 | 153 | .btn-edit { 154 | background: inherit; 155 | border: none; 156 | cursor: pointer; 157 | } 158 | 159 | .btn-edit:disabled { 160 | cursor: not-allowed !important; 161 | } 162 | 163 | .btn-clear-all { 164 | width: 100%; 165 | padding: 15px 15px; 166 | text-align: center; 167 | color: #a60ae4; 168 | font-weight: bold; 169 | margin-top: 10px; 170 | border: none; 171 | transition: all 0.05s ease-in-out; 172 | } 173 | 174 | .btn-clear-all:active { 175 | color: #de8cff; 176 | } 177 | 178 | /* Dinamic styles is start from here */ 179 | .no-item { 180 | text-align: center; 181 | color: #a2ad06; 182 | } 183 | 184 | .show-msg { 185 | display: none; 186 | text-align: center; 187 | } 188 | 189 | .form-success { 190 | display: block; 191 | color: #068a11; 192 | } 193 | 194 | .form-error { 195 | display: block; 196 | color: #f55; 197 | } 198 | 199 | .todo-edit-input { 200 | flex: 1; 201 | width: 100%; 202 | background: transparent; 203 | color: #fff; 204 | padding: 10px; 205 | cursor: text; 206 | outline: none; 207 | border: 1px solid #de8cff; 208 | border-radius: 8px; 209 | max-height: 40px; 210 | } 211 | 212 | .btn-delete { 213 | background: transparent; 214 | border-color: transparent; 215 | cursor: pointer; 216 | } 217 | 218 | .btn-delete:disabled { 219 | cursor: not-allowed; 220 | display: none; 221 | } 222 | 223 | 224 | 225 | .btn-delete:active { 226 | border-color: #de8cff; 227 | } 228 | 229 | .invalid-edit { 230 | color: #f55; 231 | border-color: #f55; 232 | } 233 | 234 | .invalid-edit:focus-visible { 235 | outline-color: #f55; 236 | } 237 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= 5 | github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 6 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 7 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 8 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 9 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 10 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 11 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 12 | github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= 13 | github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= 14 | github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= 15 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 16 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 17 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 18 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 19 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 20 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 21 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= 22 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 23 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 24 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 28 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 29 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 30 | github.com/thedevsaddam/renderer v1.2.0 h1:+N0J8t/s2uU2RxX2sZqq5NbaQhjwBjfovMU28ifX2F4= 31 | github.com/thedevsaddam/renderer v1.2.0/go.mod h1:k/TdZXGcpCpHE/KNj//P2COcmYEfL8OV+IXDX0dvG+U= 32 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 33 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 34 | github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= 35 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 36 | github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= 37 | github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= 38 | github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= 39 | github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= 40 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= 41 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 42 | go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8= 43 | go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= 44 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= 45 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 46 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 47 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 48 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 53 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 54 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 55 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 56 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 57 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 58 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 61 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 62 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 63 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 64 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 65 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 66 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | -------------------------------------------------------------------------------- /controllers/controllers.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/go-chi/chi/v5" 13 | "github.com/joho/godotenv" 14 | "github.com/justinas/nosurf" 15 | "github.com/raihan2bd/go-todo-with-mongodb/helpers" 16 | "github.com/raihan2bd/go-todo-with-mongodb/models" 17 | "github.com/thedevsaddam/renderer" 18 | "go.mongodb.org/mongo-driver/bson" 19 | "go.mongodb.org/mongo-driver/bson/primitive" 20 | "go.mongodb.org/mongo-driver/mongo" 21 | "go.mongodb.org/mongo-driver/mongo/options" 22 | ) 23 | 24 | var rnd *renderer.Render 25 | var db *mongo.Collection 26 | var ctx = context.TODO() 27 | 28 | // connect the database 29 | func init() { 30 | rnd = renderer.New() 31 | 32 | // load env file 33 | if err := godotenv.Load(); err != nil { 34 | log.Fatal("There is no env file") 35 | } 36 | 37 | // get env variable 38 | mongoUri := os.Getenv("MONGO_URI") 39 | clientOptions := options.Client().ApplyURI(mongoUri) 40 | client, err := mongo.Connect(ctx, clientOptions) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | err = client.Ping(ctx, nil) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | db = client.Database(os.Getenv("DB_NAME")).Collection(os.Getenv("COLLECTION_NAME")) 50 | } 51 | 52 | // Home Handler 53 | func HomeHandler(w http.ResponseWriter, r *http.Request) { 54 | todos, err := helpers.FetchTodosFormDB(db) 55 | if err != nil { 56 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 57 | "message": "Failed to fetch todo", 58 | "error": err, 59 | }) 60 | 61 | return 62 | } 63 | 64 | data := models.TemplateData{ 65 | CSRFToken: nosurf.Token(r), 66 | Todos: todos, 67 | } 68 | err = rnd.Template(w, http.StatusOK, []string{"views/index.html"}, data) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | } 73 | 74 | // Fetch all todos 75 | func FetchTodos(w http.ResponseWriter, r *http.Request) { 76 | todos, err := helpers.FetchTodosFormDB(db) 77 | if err != nil { 78 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 79 | "message": "Failed to fetch todo", 80 | "error": err, 81 | }) 82 | 83 | return 84 | } 85 | rnd.JSON(w, http.StatusOK, renderer.M{ 86 | "data": todos, 87 | }) 88 | } 89 | 90 | // Create todo 91 | func CreateTodo(w http.ResponseWriter, r *http.Request) { 92 | var t models.TodoModel 93 | if err := json.NewDecoder(r.Body).Decode(&t); err != nil { 94 | rnd.JSON(w, http.StatusProcessing, err) 95 | return 96 | } 97 | 98 | if t.Title == "" { 99 | rnd.JSON(w, http.StatusBadRequest, renderer.M{ 100 | "message": "The title is required", 101 | }) 102 | 103 | return 104 | } 105 | 106 | tm := models.TodoModel{ 107 | ID: t.ID, 108 | Title: t.Title, 109 | Completed: false, 110 | CreatedAt: time.Now(), 111 | } 112 | 113 | _, err := db.InsertOne(ctx, &tm) 114 | if err != nil { 115 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 116 | "message": "Failed to save", 117 | "error": err, 118 | }) 119 | return 120 | } 121 | 122 | todos, err := helpers.FetchTodosFormDB(db) 123 | if err != nil { 124 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 125 | "message": "Failed to fetch todo", 126 | "error": err, 127 | }) 128 | 129 | return 130 | } 131 | 132 | rnd.JSON(w, http.StatusCreated, renderer.M{ 133 | "message": "todo is created successfully", 134 | "todos": todos, 135 | }) 136 | } 137 | 138 | // Update todo 139 | func UpdateTodo(w http.ResponseWriter, r *http.Request) { 140 | id := strings.TrimSpace(chi.URLParam(r, "id")) 141 | 142 | objID, err := primitive.ObjectIDFromHex(id) 143 | if err != nil { 144 | rnd.JSON(w, http.StatusBadRequest, renderer.M{ 145 | "message": "The id is invalid", 146 | }) 147 | return 148 | } 149 | 150 | var t models.TodoModel 151 | 152 | if err := json.NewDecoder(r.Body).Decode(&t); err != nil { 153 | rnd.JSON(w, http.StatusProcessing, err) 154 | return 155 | } 156 | 157 | // simple validation 158 | if t.Title == "" { 159 | rnd.JSON(w, http.StatusBadRequest, renderer.M{ 160 | "message": "The title field is requried", 161 | }) 162 | return 163 | } 164 | 165 | filter := bson.M{"_id": objID} 166 | update := bson.M{"$set": bson.M{"title": t.Title, "completed": t.Completed}} 167 | _, err = db.UpdateOne(ctx, filter, update) 168 | 169 | if err != nil { 170 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 171 | "message": "Failed to update todo", 172 | "error": err, 173 | }) 174 | return 175 | } 176 | 177 | todos, err := helpers.FetchTodosFormDB(db) 178 | if err != nil { 179 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 180 | "message": "Failed to fetch todo", 181 | "error": err, 182 | }) 183 | 184 | return 185 | } 186 | 187 | rnd.JSON(w, http.StatusOK, renderer.M{ 188 | "message": "Todo updated successfully", 189 | "todos": todos, 190 | }) 191 | } 192 | 193 | // Delete One Todo 194 | func DeleteOneTodo(w http.ResponseWriter, r *http.Request) { 195 | id := strings.TrimSpace(chi.URLParam(r, "id")) 196 | 197 | objID, err := primitive.ObjectIDFromHex(id) 198 | if err != nil { 199 | rnd.JSON(w, http.StatusBadRequest, renderer.M{ 200 | "message": "The id is invalid", 201 | }) 202 | return 203 | } 204 | 205 | _, err = db.DeleteOne(ctx, bson.M{"_id": objID}) 206 | if err != nil { 207 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 208 | "message": "Failed to delete todo", 209 | "error": err, 210 | }) 211 | return 212 | } 213 | 214 | todos, err1 := helpers.FetchTodosFormDB(db) 215 | if err1 != nil { 216 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 217 | "message": "Failed to fetch todo", 218 | "error": err1, 219 | }) 220 | 221 | return 222 | } 223 | 224 | rnd.JSON(w, http.StatusOK, renderer.M{ 225 | "message": "todo is successfully deleted", 226 | "todos": todos, 227 | }) 228 | } 229 | 230 | // Delete completed todos 231 | func DeleteCompleted(w http.ResponseWriter, r *http.Request) { 232 | filter := bson.M{ 233 | "completed": bson.M{ 234 | "$eq": true, 235 | }, 236 | } 237 | _, err := db.DeleteMany(ctx, filter) 238 | if err != nil { 239 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 240 | "message": "Failed to delete completed todos", 241 | "error": err, 242 | }) 243 | return 244 | } 245 | 246 | todos, err1 := helpers.FetchTodosFormDB(db) 247 | if err1 != nil { 248 | rnd.JSON(w, http.StatusProcessing, renderer.M{ 249 | "message": "Failed to fetch todo", 250 | "error": err1, 251 | }) 252 | 253 | return 254 | } 255 | 256 | rnd.JSON(w, http.StatusOK, renderer.M{ 257 | "message": "todo is successfully deleted", 258 | "todos": todos, 259 | }) 260 | } 261 | -------------------------------------------------------------------------------- /static/app.js: -------------------------------------------------------------------------------- 1 | // Select dom element 2 | const showMsg = document.querySelector(".show-msg"); 3 | const hiddenId = document.getElementById("hidden_id"); 4 | const todoForm = document.querySelector("#todo-form"); 5 | const btnSubmit = document.querySelector(".btn-submit"); 6 | const csrf_token = document.getElementById("csrf_token"); 7 | const formInput = document.getElementById("description"); 8 | const todoItemsDes = document.querySelectorAll(".todo-des"); 9 | const filterButton = document.querySelector(".btn-clear-all"); 10 | const todoContainer = document.querySelector(".todo-items-gropu"); 11 | 12 | const todoData = []; 13 | 14 | // handle error 15 | const fireMsg = (msg, isErr = false) => { 16 | if (isErr) { 17 | showMsg.classList.add("form-error"); 18 | } else { 19 | showMsg.classList.add("form-success"); 20 | } 21 | showMsg.textContent = msg; 22 | setTimeout(() => { 23 | showMsg.classList.remove("form-error"); 24 | showMsg.classList.remove("form-success"); 25 | showMsg.textContent = ""; 26 | }, 3000); 27 | }; 28 | 29 | // fetch all todos 30 | const onFetchTodos = async () => { 31 | try { 32 | const res = await fetch("todo/"); 33 | 34 | if (!res.ok) { 35 | throw new Error(`Error: ${res.status} ${res.statusText}`); 36 | } 37 | const result = await res.json(); 38 | return result; 39 | } catch (err) { 40 | fireMsg(err, true); 41 | } 42 | }; 43 | 44 | // on add new todo data 45 | const onCreateTodo = async (title) => { 46 | try { 47 | const res = await fetch("todo/", { 48 | headers: { 49 | "Content-type": "application/json", 50 | "X-CSRF-Token": csrf_token.value, 51 | }, 52 | method: "POST", 53 | body: JSON.stringify({ 54 | title: title, 55 | completed: false, 56 | }), 57 | }); 58 | 59 | if (!res.ok) { 60 | throw new Error(`Error: ${res.status} ${res.statusText}`); 61 | } 62 | const result = await res.json(); 63 | return result; 64 | } catch (err) { 65 | fireMsg(err, true); 66 | } 67 | }; 68 | 69 | // on Update todo 70 | const onUpdateTodo = async (id, title, completed = false) => { 71 | try { 72 | const res = await fetch(`todo/${id}`, { 73 | headers: { 74 | "Content-type": "application/json", 75 | "X-CSRF-Token": csrf_token.value, 76 | }, 77 | method: "PUT", 78 | body: JSON.stringify({ 79 | title: title, 80 | completed: completed, 81 | }), 82 | }); 83 | if (!res.ok) { 84 | throw new Error(`Error: ${res.status} ${res.statusText}`); 85 | } 86 | const result = await res.json(); 87 | return result; 88 | } catch (err) { 89 | fireMsg(err, true); 90 | } 91 | }; 92 | 93 | // on Delete todo 94 | const onDeleteTodo = async (id) => { 95 | try { 96 | const res = await fetch(`todo/${id}`, { 97 | headers: { 98 | "Content-type": "application/json", 99 | "X-CSRF-Token": csrf_token.value, 100 | }, 101 | method: "DELETE", 102 | }); 103 | 104 | if (!res.ok) { 105 | throw new Error(`Error: ${res.status} ${res.statusText}`); 106 | } 107 | const result = await res.json(); 108 | return result; 109 | } catch (err) { 110 | fireMsg(err, true); 111 | } 112 | }; 113 | 114 | const onDeleteCompletedTodos = async () => { 115 | try { 116 | const res = await fetch(`todo/delete-completed`, { 117 | headers: { 118 | "X-CSRF-Token": csrf_token.value, 119 | }, 120 | method: "DELETE", 121 | }); 122 | 123 | if (!res.ok) { 124 | console.log(res); 125 | throw new Error(`Error: ${res.status} ${res.statusText}`); 126 | } 127 | const result = await res.json(); 128 | return result; 129 | } catch (err) { 130 | fireMsg(err, true); 131 | } 132 | }; 133 | 134 | // Render todos on dom 135 | const render = (todos) => { 136 | todoContainer.innerHTML = ""; 137 | if (todos.length > 0) { 138 | todos.forEach((todo) => { 139 | // create todo item 140 | const todoItem = document.createElement("li"); 141 | todoItem.id = todo.id; 142 | todoItem.className = "todo-item"; 143 | // todo checkbox 144 | const checkbox = document.createElement("input"); 145 | checkbox.setAttribute("type", "checkbox"); 146 | checkbox.id = "todo-compleate"; 147 | 148 | checkbox.addEventListener("change", (e) => { 149 | if (checkbox.getAttribute("checked")) { 150 | checkbox.removeAttribute("checked"); 151 | } else { 152 | checkbox.setAttribute("checked", "yes"); 153 | } 154 | checkbox.setAttribute("disabled", true); 155 | clickCompleateTodo( 156 | e.target.parentElement.id, 157 | e.target.nextElementSibling.innerText, 158 | e.target.getAttribute("checked") 159 | ); 160 | }); 161 | 162 | // todo description 163 | const todoDes = document.createElement("p"); 164 | todoDes.className = "todo-des"; 165 | todoDes.innerText = todo.title; 166 | 167 | // todo edit button 168 | const editBtn = document.createElement("button"); 169 | editBtn.className = "btn-edit"; 170 | editBtn.innerHTML = `...`; 171 | editBtn.addEventListener("click", (e) => { 172 | const parent = e.target.parentElement.parentElement; 173 | const getTotodes = parent.querySelector(".todo-des").innerText; 174 | formInput.value = getTotodes; 175 | document.getElementById("hidden_id").value = parent.id; 176 | formInput.focus(); 177 | }); 178 | 179 | // todo delete button 180 | const deleteBtn = document.createElement("button"); 181 | deleteBtn.className = "btn-delete"; 182 | deleteBtn.innerHTML = `Delete`; 183 | deleteBtn.addEventListener("click", (e) => { 184 | deleteEvent(e); 185 | }); 186 | 187 | if (todo.completed) { 188 | checkbox.setAttribute("checked", "yes"); 189 | todoItem.classList.add("done"); 190 | editBtn.setAttribute("disabled", ""); 191 | } 192 | 193 | // Append all the todo elements inside the todoItems 194 | todoItem.append(checkbox, todoDes, editBtn, deleteBtn); 195 | 196 | // appent the todo item inside todoContainer 197 | todoContainer.appendChild(todoItem); 198 | }); 199 | } else { 200 | todoContainer.innerHTML = 201 | '

There is no todo to show! Please add a new one.

'; 202 | } 203 | }; 204 | 205 | // submit todos 206 | const submitOntodo = async (e) => { 207 | const title = e.target.value; 208 | if (hiddenId.value.length > 1) { 209 | const result = await onUpdateTodo(hiddenId.value, title); 210 | if (result) { 211 | fireMsg(result.message); 212 | render(result.todos); 213 | hiddenId.value = ""; 214 | formInput.value = ""; 215 | } 216 | } else { 217 | const result = await onCreateTodo(title); 218 | 219 | if (result) { 220 | fireMsg(result.message); 221 | render(result.todos); 222 | formInput.value = ""; 223 | hiddenId.value = ""; 224 | } 225 | } 226 | }; 227 | 228 | // add new todos 229 | formInput.addEventListener("keypress", (e) => { 230 | if (e.key === "Enter") { 231 | e.preventDefault(); 232 | submitOntodo(e); 233 | } 234 | }); 235 | 236 | // btnSubmit 237 | btnSubmit.addEventListener("click", (e) => { 238 | e.preventDefault(); 239 | submitOntodo(e); 240 | }); 241 | 242 | const deleteEvent = async (e) => { 243 | const id = e.target.parentElement.parentElement.id; 244 | const result = await onDeleteTodo(id); 245 | e.target.setAttribute("disabled", ""); 246 | if (result) { 247 | fireMsg(result.message, false); 248 | render(result.todos); 249 | } 250 | }; 251 | 252 | // on Completed todos 253 | const clickCompleateTodo = async (id, title, comp) => { 254 | let completed = false; 255 | if (comp === "yes") { 256 | completed = true; 257 | } 258 | const result = await onUpdateTodo(id, title, completed); 259 | if (result) { 260 | render(result.todos); 261 | } 262 | }; 263 | 264 | // clear all completed todos 265 | filterButton.addEventListener("click", async () => { 266 | const result = await onDeleteCompletedTodos(); 267 | if (result) { 268 | render(result.todos); 269 | } 270 | }); 271 | 272 | // load todos on the fly 273 | window.onload = async () => { 274 | const result = await onFetchTodos(); 275 | if (result) { 276 | render(result.data); 277 | } 278 | }; 279 | --------------------------------------------------------------------------------