├── .gitignore ├── .jshint ├── README.md ├── date.go ├── date_repository.go ├── frontend ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── index.html │ ├── manifest.json │ └── robots.txt └── src │ ├── App.js │ ├── App.scss │ ├── App.test.js │ ├── assets │ ├── icons │ │ ├── add.svg │ │ ├── btn--add.png │ │ ├── btn--edit.png │ │ ├── checkbox--active.svg │ │ ├── checkbox.svg │ │ ├── hamburger.svg │ │ └── remove.svg │ └── images │ │ └── mock-pomodoro.png │ ├── components │ ├── AddActivity │ │ ├── AddActivity.jsx │ │ ├── AddActivity.scss │ │ └── index.js │ ├── AddHabit │ │ ├── AddHabit.jsx │ │ ├── AddHabit.scss │ │ └── index.js │ ├── EditHabit │ │ ├── EditHabit.jsx │ │ ├── EditHabit.scss │ │ └── index.js │ ├── Nav │ │ ├── Nav.jsx │ │ ├── Nav.scss │ │ └── index.js │ └── Pomodoro │ │ ├── Pomodoro.jsx │ │ ├── Pomodoro.scss │ │ └── index.js │ ├── index.css │ ├── index.js │ ├── pages │ └── Habit │ │ ├── Habit.jsx │ │ ├── Habit.scss │ │ └── index.js │ ├── setupTests.js │ ├── styles │ └── partials │ │ ├── _animations.scss │ │ ├── _mixins.scss │ │ ├── _typography.scss │ │ └── _variables.scss │ └── validation.js ├── go.mod ├── go.sum ├── habit.go ├── habit_repository.go ├── main.go ├── mocks └── mock_habit.go ├── project.json ├── server.go ├── tests └── habit_test.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | build/ 4 | -------------------------------------------------------------------------------- /.jshint: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![habit-tracker](https://user-images.githubusercontent.com/15822994/145180371-3d0fe074-ffb2-468f-a2c3-0fbdbcd75c83.png) 2 | 3 | ![open issues](https://img.shields.io/github/issues/bashbunni/habit-tracker) 4 | ![forks](https://img.shields.io/github/forks/bashbunni/habit-tracker) 5 | ![stars](https://img.shields.io/github/stars/bashbunni/habit-tracker) 6 | ![contributions](https://img.shields.io/badge/contributions-open-blueviolet) 7 | 8 | ## About 9 | 10 | My project is a web and desktop application that allows you to track and visualize your consistency with your habits over time with a calendar heatmap. 11 | This allows you to track any habits you’d like and see how you’re progressing towards your goals. 12 | I hope that this tool can help those who struggle with being consistent or disciplined. 13 | 14 | ![habit page](https://user-images.githubusercontent.com/15822994/145179044-4c153162-075f-4842-9068-691660c6c2bd.png) 15 | ![log activity menu](https://user-images.githubusercontent.com/15822994/145179136-37aca325-ca19-42e0-83d9-2ea3e4498729.png) 16 | 17 | ## How to Run 18 | 19 | ### Dependencies 20 | 21 | #### Frontend 22 | 23 | - all frontend dependencies are included in the `package.json` file 24 | - you can install these dependencies by navigating to the `frontend` folder then running `npm i` 25 | 26 | #### Backend 27 | 28 | - [Wails](https://wails.app/gettingstarted/) and its dependencies 29 | - Please follow their 'Getting Started' guide to install all project dependencies 30 | - Dependencies listed in `go.mod` 31 | - install these dependencies by running `go get` in the backend/root directory (same level as main.go) 32 | - Since auth isn't set up yet, you will need a `.env` file with the following values: 33 | 34 | ``` 35 | DB_USERNAME=insertusername 36 | DB_PASSWORD=insertpassword 37 | HOST=inserthost 38 | PORT=insertport 39 | DATABASE=insertdatabase 40 | ``` 41 | 42 | **_note_: You must use a MySQL db for this project**\ 43 | For example, if you're running a local MySQL database your .env file might look something like this: 44 | 45 | ``` 46 | DB_USERNAME=root 47 | DB_PASSWORD=root 48 | HOST=localhost 49 | PORT=3306 50 | DATABASE=HABIT_TRACKER 51 | ``` 52 | 53 | ### Running the App 54 | 55 | - In a terminal window, in the root folder (same folder as `main.go`) run `wails serve` to start the backend. 56 | - In a new terminal window, navigate to the frontend folder, then run `npm run serve` to start the frontend. 57 | This runs the development server for the desktop application on `http://localhost:3000` 58 | 59 | ## How to Contribute 60 | 61 | 1. Create an issue\* 62 | 2. Fork the repository 63 | 3. Create a solution that solves _exactly_ the features that are mentioned in the issue 64 | 4. Create a pull request with a link to the issue following the format: 65 | 66 | ``` 67 | ## Completed 68 | - [x] add timer functionality 69 | - [x] create timer component 70 | - [x] integrate timer React component with timer functionality 71 | 72 | ## Goal 73 | Build out the pomodoro timer as outlined in feature request issue #10 74 | ``` 75 | 76 | - the list of tasks completed will likely match your commit messages if you're writing meaningful commit messages, if not, it will add clarity to the pull request. 77 | 78 | \*issues should include a clear description. For bugs this means including screenshots of related error messages and code snippets that you suspect are involved along with your OS, Go, Wails, Node versions. 79 | For feature requests, please include a description of the feature, with a user story outlining the problem-space it aims to solve. 80 | 81 | ### Additional Information 82 | Here's an overview of the project structure: 83 | habit-tracker (2) 84 | 85 | 86 | ## Next Steps 87 | 88 | ### Add Pomodoro functionality 89 | 90 | This includes 91 | 92 | - a functional timer component 93 | - incrementing the count for a habit that has pomodoro enabled 94 | 95 | ![pomodoro page](https://user-images.githubusercontent.com/15822994/145179376-d87d1354-7497-4cef-90d0-ac7690438457.png) 96 | 97 | ### Add Authentication 98 | 99 | I would like for users to be able to keep track of their own habits and separate data by user for production. 100 | 101 | ### Release Production Build 102 | 103 | Habit Tracker is currently in release for development only, meaning that a stable production release still needs to be developed. 104 | I'm planning on migrating the project to Wails v2 which has a lot of bug fixes available. 105 | Once I've finished the migration I'll be working on building the executable desktop programs for Mac, Linux, and Windows. 106 | That will be the first major release 107 | -------------------------------------------------------------------------------- /date.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | ) 8 | 9 | type Date struct { 10 | ID string `json:"id"` 11 | Date string `json:"date"` 12 | Count uint `json:"count"` 13 | HabitID uint `json:"habit_id"` 14 | } 15 | 16 | func NewDateFromJSON(req []byte) Date { 17 | fmt.Println(req) 18 | var date Date 19 | err := json.Unmarshal(req, &date) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | return date 24 | } 25 | 26 | func NewDate(Today string, Count uint, HabitID uint) Date { 27 | return Date{Date: Today, Count: Count, HabitID: HabitID} 28 | } 29 | -------------------------------------------------------------------------------- /date_repository.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "time" 8 | ) 9 | 10 | type DateRepository interface { 11 | GetAllDates(uint) []Date 12 | GetTodaysDate() Date 13 | AddDate(Date) error 14 | AddCount(Date) error 15 | } 16 | 17 | // GetAllDates gets all dates for a given habit. 18 | func (s MySQLRepository) GetAllDates(habit_id uint) []Date { 19 | results, err := s.DB.Query("SELECT * FROM date WHERE habit_id = ?", habit_id) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | defer results.Close() 24 | var dates []Date 25 | for results.Next() { 26 | var date Date 27 | err = results.Scan(&date.ID, &date.Date, &date.Count, &date.HabitID) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | dates = append(dates, date) 32 | } 33 | return dates 34 | } 35 | 36 | // GetTodaysCountForHabit gets a Date instance for today's entry on a given habit. 37 | func (s MySQLRepository) GetTodaysCountForHabit(habitID uint) Date { 38 | today := time.Now().UTC().Format("2006-01-02") 39 | var date Date 40 | err := s.DB.QueryRow("SELECT * FROM date WHERE date_date = ? AND habit_id = ?", today, habitID).Scan(&date.ID, &date.Date, &date.Count, &date.HabitID) 41 | if err != nil { 42 | if err != sql.ErrNoRows { 43 | // real error 44 | log.Fatal(err) 45 | } 46 | } 47 | return date 48 | } 49 | 50 | func (s MySQLRepository) TodayExists(habitID uint) bool { 51 | today := s.GetTodaysCountForHabit(habitID) 52 | if today.Date == "" { 53 | return false 54 | } 55 | return true 56 | 57 | } 58 | 59 | func (s MySQLRepository) AddDate(date Date) error { 60 | fmt.Println(date.Date) 61 | insert := fmt.Sprintf("INSERT INTO date(date_date, date_count, habit_id) VALUES ('%s', %d, %d)", date.Date, date.Count, date.HabitID) 62 | fmt.Println(insert) 63 | _, err := s.DB.Exec(insert) 64 | return err 65 | } 66 | 67 | func (s MySQLRepository) AddCount(date Date) error { 68 | today := s.GetTodaysCountForHabit(date.HabitID) 69 | if s.TodayExists(date.HabitID) { 70 | updateCount := fmt.Sprintf("UPDATE date SET date_count = %d WHERE date_date = '%s' AND habit_id = %d", today.Count+date.Count, date.Date, date.HabitID) 71 | _, err := s.DB.Exec(updateCount) 72 | return err 73 | } else { 74 | return s.AddDate(date) 75 | } 76 | } 77 | 78 | func (s MySQLRepository) AddCountFromJSON(req []byte) error { 79 | return s.AddCount(JSONToDate(req)) 80 | } 81 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "habit_tracker", 3 | "author": "bashbunni", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@wailsapp/runtime": "^1.0.10", 8 | "axios": "^0.24.0", 9 | "core-js": "^3.6.4", 10 | "react": "^17.0.2", 11 | "react-calendar-heatmap": "^1.8.1", 12 | "react-dom": "^17.0.2", 13 | "react-router-dom": "5.3", 14 | "react-scripts": "4.0.3", 15 | "react-tooltip": "^4.2.21", 16 | "sass": "^1.43.4", 17 | "wails-react-scripts": "3.0.1-2", 18 | "web-vitals": "^1.1.2" 19 | }, 20 | "scripts": { 21 | "serve": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Habit Tracker 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 3 | import Nav from "./components/Nav"; 4 | import Habit from "./pages/Habit"; 5 | import Pomodoro from "./components/Pomodoro"; 6 | import AddHabit from "./components/AddHabit"; 7 | import hamburger from "./assets/icons/hamburger.svg"; 8 | import "./App.scss"; 9 | 10 | const App = () => { 11 | const [habitList, setHabitList] = useState([]); 12 | const [nav, setNav] = useState(false); 13 | const openNav = () => setNav(true); 14 | const closeNav = () => setNav(false); 15 | const mountedRef = useRef(true); 16 | 17 | function updateHabits() { 18 | window.backend.MySQLRepository.GetAllHabits().then((response) => { 19 | setHabitList(response); 20 | }); 21 | } 22 | 23 | useEffect(() => { 24 | if (mountedRef.current) { 25 | updateHabits(); 26 | } 27 | return () => (mountedRef.current = false); 28 | }, []); 29 | 30 | return ( 31 |
32 | {habitList && ( 33 |
34 | open menu 40 | 41 | {nav &&
68 | )} 69 |
70 | ); 71 | }; 72 | 73 | export default App; 74 | -------------------------------------------------------------------------------- /frontend/src/App.scss: -------------------------------------------------------------------------------- 1 | @use "./styles/partials/variables" as *; 2 | @use "./styles/partials/mixins" as *; 3 | @use "./styles/partials/typography" as *; 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | font-family: $font-family; 11 | background-color: $background; 12 | width: 100vw; 13 | height: 100vh; 14 | color: white; 15 | } 16 | 17 | a { 18 | text-decoration: none; 19 | color: inherit; 20 | } 21 | 22 | ul { 23 | list-style: none; 24 | } 25 | 26 | p { 27 | font-size: 1rem; 28 | } 29 | 30 | .app { 31 | display: flex; 32 | justify-content: center; 33 | width: 100%; 34 | } 35 | 36 | .container { 37 | width: 100%; 38 | margin: 2rem; 39 | @include tablet { 40 | max-width: $container-max; 41 | } 42 | } 43 | 44 | .hamburger { 45 | width: 30px; 46 | height: 30px; 47 | cursor: pointer; 48 | &--hidden { 49 | visibility: hidden; 50 | cursor: auto; 51 | } 52 | } 53 | 54 | .highlight { 55 | font-weight: 800; 56 | } 57 | 58 | .form { 59 | display: flex; 60 | flex-direction: column; 61 | &__title { 62 | @include subtitle(white); 63 | &--add-habit { 64 | @include maintitle; 65 | @include tablet { 66 | align-self: center; 67 | } 68 | } 69 | } 70 | & label { 71 | margin: 0.5rem 0; 72 | text-transform: uppercase; 73 | } 74 | &-field { 75 | @include form-field; 76 | width: 50%; 77 | } 78 | &__input { 79 | @include form-field; 80 | @include divider($grey); 81 | background-color: white; 82 | height: 40px; 83 | font-family: inherit; 84 | &--error { 85 | border: 1px solid $red; 86 | } 87 | } 88 | &__btn { 89 | @include btn($light-green); 90 | font-family: inherit; 91 | &-container { 92 | @include tablet { 93 | display: flex; 94 | justify-content: flex-start; 95 | flex-direction: row-reverse; 96 | } 97 | } 98 | &--reset { 99 | @include btn($grey); 100 | } 101 | &--delete { 102 | @include btn($red); 103 | } 104 | &--error { 105 | border: none; 106 | background-color: $grey; 107 | color: white; 108 | @include tablet { 109 | max-width: 200px; 110 | } 111 | &:hover { 112 | background-color: $red; 113 | cursor: not-allowed; 114 | } 115 | } 116 | } 117 | } 118 | 119 | .close { 120 | align-self: flex-end; 121 | width: 25px; 122 | height: 25px; 123 | cursor: pointer; 124 | &--nav { 125 | margin: 1.5rem 1.5rem 150px 0; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/btn--add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashbunni/habit-tracker/b13287f470e8cb23099510267651d70c3ec4c239/frontend/src/assets/icons/btn--add.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/btn--edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashbunni/habit-tracker/b13287f470e8cb23099510267651d70c3ec4c239/frontend/src/assets/icons/btn--edit.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/checkbox--active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/checkbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/icons/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/images/mock-pomodoro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashbunni/habit-tracker/b13287f470e8cb23099510267651d70c3ec4c239/frontend/src/assets/images/mock-pomodoro.png -------------------------------------------------------------------------------- /frontend/src/components/AddActivity/AddActivity.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./AddActivity.scss"; 3 | 4 | import close from "../../assets/icons/remove.svg"; 5 | import "./AddActivity.scss"; 6 | 7 | const AddActivity = ({ habit_id, unit, setAddOpen, getDates }) => { 8 | const incrementCount = (e) => { 9 | e.preventDefault(); 10 | let today = new Date().toISOString().slice(0, 10); 11 | window.backend 12 | .NewDate(today, Number(e.target.count.value), habit_id) 13 | .then((response) => { 14 | console.log(response); 15 | window.backend.MySQLRepository.AddCountFromJSON( 16 | JSON.stringify(response) 17 | ) 18 | .then(() => { 19 | getDates(); 20 | setAddOpen(false); 21 | }) 22 | .catch((err) => { 23 | console.error(err); 24 | }); 25 | }); 26 | }; 27 | 28 | return ( 29 |
30 | close { 35 | setAddOpen(false); 36 | }} 37 | /> 38 |

Log Activity

39 |
40 | 41 | 48 |
49 | 52 | 61 |
62 |
63 |
64 | ); 65 | }; 66 | 67 | export default AddActivity; 68 | -------------------------------------------------------------------------------- /frontend/src/components/AddActivity/AddActivity.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/partials/_mixins.scss" as *; 2 | @use "../../styles/partials/_variables.scss" as *; 3 | 4 | .add-activity { 5 | @include modal($blue); 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/components/AddActivity/index.js: -------------------------------------------------------------------------------- 1 | import AddActivity from './AddActivity.jsx'; 2 | 3 | export default AddActivity; 4 | -------------------------------------------------------------------------------- /frontend/src/components/AddHabit/AddHabit.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import { isValidName, isValidForm } from "../../validation.js"; 4 | import "./AddHabit.scss"; 5 | 6 | const AddHabit = ({ updateHabits, habitList }) => { 7 | const [habit, setHabit] = useState({}); 8 | const history = useHistory(); 9 | 10 | const addHabit = () => { 11 | window.backend 12 | .NewHabit(habit.id, habit.name, habit.unit, habit.pomodoro, habit.why) 13 | .then((response) => { 14 | window.backend.MySQLRepository.AddHabitFromJSON( 15 | JSON.stringify(response) 16 | ) 17 | .then(() => { 18 | updateHabits(); 19 | history.push(`/${habit.name}`); 20 | }) 21 | .catch((err) => console.error(err)); 22 | }); 23 | }; 24 | 25 | return ( 26 |
27 |
{ 30 | e.preventDefault(); 31 | addHabit(); 32 | }} 33 | > 34 |

Add Habit

35 | 38 | setHabit({ ...habit, name: e.target.value })} 49 | /> 50 | 53 | setHabit({ ...habit, unit: e.target.value })} 62 | /> 63 | 66 | setHabit({ ...habit, why: e.target.value })} 75 | /> 76 |
77 | {isValidForm(habit, habitList) ? ( 78 | 81 | ) : ( 82 | 89 | )} 90 | 96 |
97 |
98 |
99 | ); 100 | }; 101 | 102 | export default AddHabit; 103 | -------------------------------------------------------------------------------- /frontend/src/components/AddHabit/AddHabit.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/partials/_mixins.scss" as *; 2 | @use "../../styles/partials/_variables.scss" as *; 3 | 4 | .add-habit { 5 | display: flex; 6 | flex-direction: column; 7 | max-width: $container-max; 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/components/AddHabit/index.js: -------------------------------------------------------------------------------- 1 | import AddHabit from "./AddHabit.jsx"; 2 | 3 | export default AddHabit; 4 | -------------------------------------------------------------------------------- /frontend/src/components/EditHabit/EditHabit.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import { isValidName, isValidForm } from "../../validation.js"; 4 | import close from "../../assets/icons/remove.svg"; 5 | import "./EditHabit.scss"; 6 | 7 | const EditHabit = ({ 8 | habit, 9 | setHabit, 10 | setEditOpen, 11 | updateHabits, 12 | habitList, 13 | }) => { 14 | const [tempHabit, setTempHabit] = useState(habit); 15 | const history = useHistory(); 16 | 17 | const deleteHabit = () => { 18 | window.backend.MySQLRepository.DeleteHabit(habit.id).catch((err) => 19 | console.error(err) 20 | ); 21 | updateHabits(); 22 | history.push("/"); 23 | }; 24 | 25 | const EditHabit = () => { 26 | window.backend.MySQLRepository.EditHabitFromJSON(JSON.stringify(tempHabit)) 27 | .then(() => { 28 | updateHabits(); 29 | history.push(`/${tempHabit.name}`); 30 | setEditOpen(false); 31 | }) 32 | .catch((err) => console.error(err)); 33 | }; 34 | 35 | return ( 36 |
37 | close { 42 | setEditOpen(false); 43 | }} 44 | /> 45 |
{ 48 | e.preventDefault(); 49 | EditHabit(); 50 | setHabit({ 51 | name: tempHabit.name, 52 | unit: tempHabit.unit, 53 | why: tempHabit.why, 54 | }); 55 | }} 56 | > 57 |

Edit Habit

58 | 61 | setTempHabit({ ...tempHabit, name: e.target.value })} 73 | /> 74 | 77 | setTempHabit({ ...tempHabit, unit: e.target.value })} 87 | /> 88 | 91 | setTempHabit({ ...tempHabit, why: e.target.value })} 101 | /> 102 |
103 | {isValidForm(tempHabit, habitList) ? ( 104 | 107 | ) : ( 108 | 115 | )} 116 | 125 | 128 |
129 |
130 |
131 | ); 132 | }; 133 | 134 | export default EditHabit; 135 | -------------------------------------------------------------------------------- /frontend/src/components/EditHabit/EditHabit.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/partials/_mixins.scss" as *; 2 | @use "../../styles/partials/_variables.scss" as *; 3 | 4 | .edit-habit { 5 | @include modal($light-green); 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/src/components/EditHabit/index.js: -------------------------------------------------------------------------------- 1 | import EditHabit from "./EditHabit.jsx"; 2 | 3 | export default EditHabit; 4 | -------------------------------------------------------------------------------- /frontend/src/components/Nav/Nav.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import "./Nav.scss"; 4 | import close from "../../assets/icons/remove.svg"; 5 | 6 | const Nav = ({ closeNav, habitList }) => { 7 | return ( 8 |
9 | close modal 15 | 34 |
35 | ); 36 | }; 37 | 38 | export default Nav; 39 | -------------------------------------------------------------------------------- /frontend/src/components/Nav/Nav.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/partials/variables" as *; 2 | @use "../../styles/partials/mixins" as *; 3 | 4 | .sidebar { 5 | margin: 0; 6 | width: 100%; 7 | height: 100%; 8 | position: absolute; 9 | display: flex; 10 | flex-direction: column; 11 | top: 0; 12 | left: 0; 13 | min-width: $modal-width; 14 | background-color: $black; 15 | z-index: 1; 16 | @include tablet { 17 | width: $modal-width; 18 | } 19 | } 20 | 21 | .nav { 22 | margin: 0; 23 | &__item { 24 | margin-bottom: 1rem; 25 | } 26 | &__link { 27 | @include subtitle(white); 28 | &--active { 29 | @include subtitle($light-green); 30 | } 31 | &--pomodoro { 32 | @include subtitle($blue); 33 | } 34 | &--add { 35 | background: url("../../assets/icons/add.svg"); 36 | background-repeat: no-repeat; 37 | background-size: 25px; 38 | background-position: 0 12.5px; 39 | padding-left: 35px; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/Nav/index.js: -------------------------------------------------------------------------------- 1 | import Nav from "./Nav.jsx"; 2 | 3 | export default Nav; 4 | -------------------------------------------------------------------------------- /frontend/src/components/Pomodoro/Pomodoro.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from "react"; 2 | import axios from "axios"; 3 | import mockPomodoro from "../../assets/images/mock-pomodoro.png"; 4 | import "./Pomodoro.scss"; 5 | 6 | const Pomodoro = () => { 7 | const [quote, setQuote] = useState({}); 8 | const timeRemaining = "10 minutes 58 seconds"; 9 | let mountedRef = useRef(true); 10 | 11 | useEffect(() => { 12 | axios.get("https://type.fit/api/quotes").then(({ data }) => { 13 | if (mountedRef.current) { 14 | let random = Math.floor(Math.random() * data.length); 15 | setQuote(data[random]); 16 | } 17 | }); 18 | return () => (mountedRef.current = false); 19 | }, []); 20 | 21 | return ( 22 |
23 |

pomodoro

24 |

25 | Time Left: 26 | {timeRemaining} 27 |

28 | pomodoro 29 |
30 |
31 |

32 | "{quote.text}"
-{quote.author ? quote.author : "unknown"} 33 |

34 |
35 |
36 |
reset
37 |
start
38 |
39 |
40 |
41 | ); 42 | }; 43 | 44 | export default Pomodoro; 45 | -------------------------------------------------------------------------------- /frontend/src/components/Pomodoro/Pomodoro.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/partials/mixins" as *; 2 | @use "../../styles/partials/variables" as *; 3 | 4 | .pomodoro { 5 | height: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | &-box { 10 | @include divider($blue); 11 | border-radius: $default-border; 12 | padding: 0; 13 | } 14 | &__title { 15 | @include subtitle($blue); 16 | } 17 | &__image { 18 | width: 90%; 19 | } 20 | &__quote { 21 | max-width: 100%; 22 | margin: 0; 23 | } 24 | &__bottom { 25 | display: flex; 26 | flex-direction: column; 27 | width: 90%; 28 | margin: 2rem 0; 29 | @include tablet { 30 | flex-direction: row; 31 | align-items: flex-end; 32 | justify-content: space-between; 33 | } 34 | } 35 | &__buttons { 36 | display: flex; 37 | flex-direction: column-reverse; 38 | @include tablet { 39 | flex-direction: row; 40 | } 41 | } 42 | &__btn { 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | padding: 0.5rem 1rem; 47 | width: 100%; 48 | margin-top: 1.5rem; 49 | @include tablet { 50 | margin-left: 2rem; 51 | min-width: 120px; 52 | min-height: 50px; 53 | max-width: 120px; 54 | max-height: 50px; 55 | } 56 | &--reset { 57 | @include btn($grey); 58 | } 59 | &--start { 60 | @include btn($blue); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/components/Pomodoro/index.js: -------------------------------------------------------------------------------- 1 | import Pomodoro from "./Pomodoro.jsx"; 2 | 3 | export default Pomodoro; 4 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | import * as Wails from "@wailsapp/runtime"; 7 | 8 | Wails.Init(() => { 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById("root") 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/pages/Habit/Habit.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import CalendarHeatmap from "react-calendar-heatmap"; 3 | import ReactTooltip from "react-tooltip"; 4 | import edit from "../../assets/icons/btn--edit.png"; 5 | import add from "../../assets/icons/btn--add.png"; 6 | import AddActivity from "../../components/AddActivity"; 7 | import EditHabit from "../../components/EditHabit"; 8 | import Pomodoro from "../../components/Pomodoro"; 9 | import "./Habit.scss"; 10 | 11 | const today = new Date().toISOString().substring(0, 10); 12 | 13 | const Habit = ({ habitList, updateHabits }) => { 14 | const url = window.location.pathname.split("/").pop(); 15 | const [addOpen, setAddOpen] = useState(false); 16 | const [editOpen, setEditOpen] = useState(false); 17 | const [habit, setHabit] = useState({}); 18 | const [dates, setDates] = useState([]); 19 | 20 | const defaultValues = getRange(364).map((index) => { 21 | return { 22 | date: shiftDate(today, -index), 23 | count: 0, 24 | }; 25 | }); 26 | 27 | const getDates = () => { 28 | window.backend.MySQLRepository.GetAllDates(habit.id).then((response) => { 29 | Array.prototype.push.apply(defaultValues, response); 30 | setDates(defaultValues); 31 | }); 32 | }; 33 | 34 | useEffect(() => { 35 | setHabit(habitList[0]); 36 | }, [habitList]); 37 | 38 | // runs on start and when habit updates 39 | useEffect(() => { 40 | if (habit && habit.id) { 41 | getDates(); 42 | } 43 | }, [habit]); 44 | 45 | useEffect(() => { 46 | const myHabit = habitList.find((habit) => habit.name === url); 47 | if (myHabit !== undefined) { 48 | setHabit(myHabit); 49 | } 50 | }, [url, habitList]); 51 | 52 | return ( 53 | <> 54 | {habit && ( 55 |
56 | {addOpen && ( 57 | 63 | )} 64 | {editOpen && ( 65 | 72 | )} 73 |
74 |

{habit.name}

75 | edit habit { 80 | setEditOpen(true); 81 | }} 82 | /> 83 | add activity { 88 | setAddOpen(true); 89 | }} 90 | /> 91 |
92 |
93 |
94 |

why

95 |

{habit.why}

96 |
97 |
98 |

goal:

99 |
100 |

daily

101 |

weekly

102 |

103 | occasionally 104 |

105 |
106 |
107 |
108 | {dates && ( 109 | { 114 | if (!value) { 115 | return "color-empty"; 116 | } 117 | return `color-github-${value.count}`; 118 | }} 119 | tooltipDataAttrs={(value) => { 120 | return { 121 | "data-tip": `${value.date}: ${value.count} ${habit.unit}`, 122 | }; 123 | }} 124 | showWeekdayLabels={true} 125 | /> 126 | )} 127 | 128 |
129 | 130 |
131 |
132 | )} 133 | 134 | ); 135 | }; 136 | 137 | /* ******** 138 | * helpers 139 | * ********/ 140 | 141 | function shiftDate(date, numDays) { 142 | const newDate = new Date(date); 143 | newDate.setDate(newDate.getDate() + numDays); 144 | return newDate.toISOString().substring(0, 10); 145 | } 146 | 147 | function getRange(count) { 148 | return Array.from({ length: count }, (_, i) => i); 149 | } 150 | 151 | export default Habit; 152 | -------------------------------------------------------------------------------- /frontend/src/pages/Habit/Habit.scss: -------------------------------------------------------------------------------- 1 | @use "../../styles/partials/mixins" as *; 2 | @use "../../styles/partials/variables" as *; 3 | @use "../../styles/partials/animations" as *; 4 | 5 | .habit { 6 | &__header { 7 | width: 100%; 8 | display: flex; 9 | justify-content: flex-end; 10 | align-items: center; 11 | margin-top: 2rem; 12 | } 13 | &__title { 14 | @include maintitle; 15 | margin: 0; 16 | margin-right: auto; 17 | @include tablet { 18 | margin: 0 auto; 19 | position: relative; 20 | left: 54px; 21 | } 22 | } 23 | &__icon { 24 | margin-left: 1.5rem; 25 | max-width: 30px; 26 | max-height: 30px; 27 | cursor: pointer; 28 | } 29 | &__about { 30 | display: flex; 31 | justify-content: space-between; 32 | align-items: flex-start; 33 | margin-top: 3rem; 34 | @include divider($light-green); 35 | border-radius: $default-border; 36 | padding: 1rem; 37 | @include tablet { 38 | padding: 1rem 3rem; 39 | } 40 | } 41 | } 42 | 43 | .why { 44 | display: flex; 45 | flex-direction: column; 46 | &__title { 47 | margin: 0; 48 | text-transform: uppercase; 49 | } 50 | } 51 | 52 | .goal { 53 | display: flex; 54 | &__title { 55 | text-transform: uppercase; 56 | margin: 0; 57 | } 58 | &__choice { 59 | margin: 0; 60 | background: url("../../assets/icons/checkbox.svg"); 61 | background-repeat: no-repeat; 62 | background-size: 15px; 63 | padding-left: 30px; 64 | margin-left: 5px; 65 | background-position: 7px; 66 | &--active { 67 | background: url("../../assets/icons/checkbox--active.svg"); 68 | background-repeat: no-repeat; 69 | background-size: 15px; 70 | background-position: 7px; 71 | } 72 | } 73 | } 74 | 75 | /* ******** 76 | * heatmap 77 | * ********/ 78 | 79 | .react-calendar-heatmap { 80 | margin-top: 3rem; 81 | width: 100%; 82 | & text { 83 | font-size: 0.5rem; 84 | fill: white; 85 | } 86 | & .color-empty { 87 | fill: #eeeeee; 88 | } 89 | } 90 | 91 | rect { 92 | rx: 1px; 93 | animation: load 0.5s; 94 | } 95 | 96 | /* ******** 97 | * tooltip 98 | * ********/ 99 | 100 | .__react_component_tooltip { 101 | font-size: 1rem !important; 102 | // important is necessary here because the component uses inline styles 103 | } 104 | 105 | /* *********************** 106 | * colours based on count 107 | * ***********************/ 108 | 109 | .react-calendar-heatmap .color-github-0 { 110 | fill: #eeeeee; 111 | } 112 | .react-calendar-heatmap .color-github-1 { 113 | fill: #d6e685; 114 | } 115 | .react-calendar-heatmap .color-github-2 { 116 | fill: #8cc665; 117 | } 118 | .react-calendar-heatmap .color-github-3 { 119 | fill: #44a340; 120 | } 121 | .react-calendar-heatmap .color-github-4 { 122 | fill: #1e6823; 123 | } 124 | -------------------------------------------------------------------------------- /frontend/src/pages/Habit/index.js: -------------------------------------------------------------------------------- 1 | import Habit from "./Habit.jsx"; 2 | 3 | export default Habit; 4 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/src/styles/partials/_animations.scss: -------------------------------------------------------------------------------- 1 | /* ***************** 2 | * habit animations 3 | * *****************/ 4 | 5 | @keyframes load { 6 | from { 7 | transform: scale(0); 8 | } 9 | to { 10 | transform: scale(1); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/styles/partials/_mixins.scss: -------------------------------------------------------------------------------- 1 | @use "variables" as *; 2 | 3 | /* ************** 4 | * media queries 5 | * **************/ 6 | 7 | @mixin tablet { 8 | @media (min-width: $tablet-size) { 9 | @content; 10 | } 11 | } 12 | 13 | /* *********** 14 | * formatting 15 | * ***********/ 16 | 17 | @mixin divider($color) { 18 | border: $color 1px solid; 19 | } 20 | 21 | @mixin maintitle { 22 | text-transform: uppercase; 23 | color: white; 24 | font-size: 3rem; 25 | } 26 | 27 | @mixin subtitle($color) { 28 | text-transform: uppercase; 29 | color: $color; 30 | font-size: 2.25rem; 31 | } 32 | 33 | @mixin btn($color) { 34 | border: $color 1px solid; 35 | border-radius: $default-border; 36 | color: $color; 37 | background-color: $background; 38 | font-size: 1.125rem; 39 | text-transform: uppercase; 40 | font-weight: 800; 41 | cursor: pointer; 42 | padding: 0.5rem 1rem; 43 | width: 100%; 44 | margin-top: 1rem; 45 | @include tablet { 46 | min-width: 120px; 47 | min-height: 50px; 48 | max-width: 120px; 49 | max-height: 50px; 50 | margin-left: 2rem; 51 | } 52 | &:hover { 53 | background-color: $color; 54 | color: white; 55 | } 56 | } 57 | 58 | @mixin form-field { 59 | min-height: $form-field-height; 60 | border: $grey 1px solid; 61 | background-color: white; 62 | border-radius: $default-border; 63 | padding-left: 1rem; 64 | } 65 | 66 | @mixin modal($color) { 67 | @include divider($color); 68 | position: absolute; 69 | border-radius: $default-border; 70 | background-color: $black; 71 | max-width: $modal-max; 72 | width: 80%; 73 | top: 50%; 74 | left: 50%; 75 | transform: translate(-50%, -50%); 76 | padding: 3rem; 77 | display: flex; 78 | flex-direction: column; 79 | z-index: 1; 80 | } 81 | -------------------------------------------------------------------------------- /frontend/src/styles/partials/_typography.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Red+Hat+Display:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); 2 | 3 | $font-family: "Red Hat Display", sans-serif; 4 | -------------------------------------------------------------------------------- /frontend/src/styles/partials/_variables.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | /* ********** 4 | * formulas 5 | * **********/ 6 | @function toRem($value) { 7 | $remValue: math.div($value, 16) + rem; 8 | @return $remValue; 9 | } 10 | 11 | /* ******* 12 | * colors 13 | * *******/ 14 | $light-green: #6bbb81; 15 | $background: #313131; 16 | $pink: #ffa7e6; 17 | $blue: #87d4ff; 18 | $black: #292929; 19 | $grey: #a3a3a3; 20 | $red: #f94d50; 21 | 22 | /* ******* 23 | * layout 24 | * *******/ 25 | $container-max: 960px; 26 | $modal-max: 720px; 27 | $m-default-padding: 2rem; 28 | $tablet-size: 768px; 29 | $modal-width: 350px; 30 | $default-border: 10px; 31 | $form-field-height: 25px; 32 | $btn-height: 50px; 33 | 34 | /* *********** 35 | * typography 36 | * ***********/ 37 | 38 | $m-body-size: toRem(12px); 39 | $dt-body-size: toRem(14px); 40 | $body-weight: 400; 41 | $body-height: toRem(20px); 42 | 43 | $subheader-size: 600; 44 | $subheader-weight: 600; 45 | 46 | $header-size: toRem(22px); 47 | $header-weight: 600; 48 | $body-height: toRem(28px); 49 | -------------------------------------------------------------------------------- /frontend/src/validation.js: -------------------------------------------------------------------------------- 1 | const isValidName = (name, habitList) => { 2 | let exists = habitList.filter((habit) => habit.name === name); 3 | return name && exists.length <= 1; 4 | }; 5 | 6 | const isValidForm = (habit, habitList) => { 7 | return isValidName(habit.name, habitList) && habit.unit && habit.why; 8 | }; 9 | 10 | module.exports = { isValidName, isValidForm }; 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module habit_tracker 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.6.0 7 | github.com/joho/godotenv v1.4.0 8 | github.com/pkg/errors v0.8.1 9 | github.com/wailsapp/wails v1.16.8 10 | ) 11 | 12 | require ( 13 | github.com/Masterminds/semver v1.4.2 // indirect 14 | github.com/abadojack/whatlanggo v1.0.1 // indirect 15 | github.com/fatih/color v1.7.0 // indirect 16 | github.com/go-playground/colors v1.2.0 // indirect 17 | github.com/gorilla/websocket v1.4.1 // indirect 18 | github.com/jackmordaunt/icns v1.0.0 // indirect 19 | github.com/kennygrant/sanitize v1.2.4 // indirect 20 | github.com/leaanthony/slicer v1.4.0 // indirect 21 | github.com/leaanthony/spinner v0.5.3 // indirect 22 | github.com/leaanthony/synx v0.1.0 // indirect 23 | github.com/leaanthony/wincursor v0.1.0 // indirect 24 | github.com/mattn/go-colorable v0.1.1 // indirect 25 | github.com/mattn/go-isatty v0.0.7 // indirect 26 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect 27 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 // indirect 28 | github.com/sirupsen/logrus v1.8.1 // indirect 29 | github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba // indirect 30 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect 31 | golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect 32 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect 33 | golang.org/x/text v0.3.0 // indirect 34 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= 2 | github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 3 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= 4 | github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4= 5 | github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 10 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 11 | github.com/go-playground/colors v1.2.0 h1:0EdjTXKrr2g1L/LQTYtIqabeHpZuGZz1U4osS1T8+5M= 12 | github.com/go-playground/colors v1.2.0/go.mod h1:miw1R2JIE19cclPxsXqNdzLZsk4DP4iF+m88bRc7kfM= 13 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 14 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 15 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 16 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 17 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= 18 | github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ= 19 | github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo= 20 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 21 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 22 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 23 | github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= 24 | github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= 25 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 26 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 27 | github.com/leaanthony/slicer v1.4.0 h1:Q9u4w+UBU4WHjXnEDdz+eRLMKF/rnyosRBiqULnc1J8= 28 | github.com/leaanthony/slicer v1.4.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= 29 | github.com/leaanthony/spinner v0.5.3 h1:IMTvgdQCec5QA4qRy0wil4XsRP+QcG1OwLWVK/LPZ5Y= 30 | github.com/leaanthony/spinner v0.5.3/go.mod h1:oHlrvWicr++CVV7ALWYi+qHk/XNA91D9IJ48IqmpVUo= 31 | github.com/leaanthony/synx v0.1.0 h1:R0lmg2w6VMb8XcotOwAe5DLyzwjLrskNkwU7LLWsyL8= 32 | github.com/leaanthony/synx v0.1.0/go.mod h1:Iz7eybeeG8bdq640iR+CwYb8p+9EOsgMWghkSRyZcqs= 33 | github.com/leaanthony/wincursor v0.1.0 h1:Dsyp68QcF5cCs65AMBmxoYNEm0n8K7mMchG6a8fYxf8= 34 | github.com/leaanthony/wincursor v0.1.0/go.mod h1:7TVwwrzSH/2Y9gLOGH+VhA+bZhoWXBRgbGNTMk+yimE= 35 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 36 | github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= 37 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 38 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 39 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 40 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 41 | github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= 42 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 43 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 44 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= 45 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 46 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= 47 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 48 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 49 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 50 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 51 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 53 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 54 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 55 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 56 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 57 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 58 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 59 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 60 | github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba h1:2DHfQOxcpWdGf5q5IzCUFPNvRX9Icf+09RvQK2VnJq0= 61 | github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba/go.mod h1:iLnlXG2Pakcii2CU0cbY07DRCSvpWNa7nFxtevhOChk= 62 | github.com/wailsapp/wails v1.16.8 h1:wTu1v/z0ghj8NGlVc0+IxgTAU0KXZKrJTWJZX/wlvkU= 63 | github.com/wailsapp/wails v1.16.8/go.mod h1:c7s9ZxF5iPMvW2Cz9C6Brhjjnk5LuUb1HsIve4UYzhk= 64 | golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 65 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 66 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 67 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= 68 | golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 69 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 70 | golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= 71 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 72 | golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73 | golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 75 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 76 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik= 80 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 82 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 83 | gopkg.in/AlecAivazis/survey.v1 v1.8.4/go.mod h1:iBNOmqKz/NUbZx3bA+4hAGLRC7fSK7tgtVDT4tB22XA= 84 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 h1:0efs3hwEZhFKsCoP8l6dDB1AZWMgnEl3yWXWRZTOaEA= 87 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 88 | -------------------------------------------------------------------------------- /habit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Habit struct { 4 | // need to start with an uppercase letter to export it 5 | ID uint `json:"id"` 6 | Name string `json:"name"` 7 | Unit string `json:"unit"` 8 | Pomodoro bool `json:"pomodoro"` 9 | Why string `json:"why"` 10 | } 11 | 12 | // create a new Habit 13 | func NewHabit(ID uint, Name string, Unit string, Pomodoro bool, Why string) Habit { 14 | return Habit{ID: ID, Name: Name, Unit: Unit, Pomodoro: Pomodoro, Why: Why} 15 | } 16 | -------------------------------------------------------------------------------- /habit_repository.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | ) 8 | 9 | type HabitRepository interface { 10 | GetAllHabits() []Habit 11 | GetHabit(uint) Habit 12 | AddHabit(Habit) error 13 | EditHabit(Habit) Habit 14 | DeleteHabit(uint) bool 15 | } 16 | 17 | type MySQLRepository struct { 18 | DB *sql.DB 19 | } 20 | 21 | func NewMySQLConnection() *MySQLRepository { 22 | s := MySQLRepository{DB: DBConnect()} 23 | return &s 24 | } 25 | 26 | // GetHabits retrieves the list of Habits. 27 | func (s MySQLRepository) GetAllHabits() []Habit { 28 | results, err := s.DB.Query("SELECT * FROM habit") 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | defer results.Close() 33 | var habits []Habit 34 | for results.Next() { 35 | var habit Habit 36 | err = results.Scan(&habit.ID, &habit.Name, &habit.Unit, &habit.Pomodoro, &habit.Why) 37 | habits = append(habits, habit) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | } 42 | return habits 43 | } 44 | 45 | // GetHabit gets a habit with the given ID. 46 | func (s MySQLRepository) GetHabit(id uint) Habit { 47 | var result Habit 48 | err := s.DB.QueryRow("SELECT * FROM habit WHERE habit_id = ?", id).Scan(&result.ID, &result.Name, &result.Unit, &result.Pomodoro, &result.Why) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | return result 53 | } 54 | 55 | // AddHabitFromJSON adds a habit given a JSON value of a habit object 56 | func (s *MySQLRepository) AddHabitFromJSON(req []byte) error { 57 | return s.AddHabit(JSONToHabit(req)) 58 | } 59 | 60 | // AddHabit add a new habit to the database 61 | func (s MySQLRepository) AddHabit(habit Habit) error { 62 | fmt.Println(habit) 63 | 64 | insert := fmt.Sprintf("INSERT INTO habit(habit_id, habit_name, habit_unit, habit_pomodoro, habit_why) VALUES ('%d', '%s', '%s', '%v', '%s')", habit.ID, habit.Name, habit.Unit, habit.Pomodoro, habit.Why) 65 | _, err := s.DB.Exec(insert) 66 | return err 67 | } 68 | 69 | func (s MySQLRepository) EditHabitFromJSON(req []byte) error { 70 | return s.EditHabit(JSONToHabit(req)) 71 | } 72 | 73 | func (s MySQLRepository) EditHabit(habit Habit) error { 74 | _, err := s.DB.Exec("UPDATE habit SET habit_id = ?, habit_name = ?, habit_unit = ?, habit_pomodoro = ?, habit_why = ? WHERE habit_id = ?", habit.ID, habit.Name, habit.Unit, habit.Pomodoro, habit.Why, habit.ID) 75 | return err 76 | } 77 | 78 | func (s MySQLRepository) DeleteHabit(id uint) bool { 79 | _, err := s.DB.Exec("DELETE FROM habit WHERE habit_id = ?", id) 80 | if err != nil { 81 | log.Fatal(err) 82 | } 83 | return true 84 | } 85 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/wailsapp/wails" 7 | ) 8 | 9 | func main() { 10 | /* 11 | mysql := NewMySQLConnection() 12 | fmt.Println(mysql.TodayExists()) 13 | sample := Date{"2021-12-04", 2, 1} 14 | sampleJson, err := json.Marshal(sample) 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | fmt.Printf("%s", sampleJson) 19 | err = mysql.AddCountFromJSON(sampleJson) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | */ 24 | app := wails.CreateApp(&wails.AppConfig{ 25 | Width: 1024, 26 | Height: 768, 27 | Title: "habit_tracker", 28 | Colour: "#131313", 29 | }) 30 | app.Bind(NewMySQLConnection()) 31 | app.Bind(NewHabit) 32 | app.Bind(NewDate) 33 | app.Bind(JSONToHabit) 34 | app.Run() 35 | } 36 | -------------------------------------------------------------------------------- /mocks/mock_habit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type MockHabitRepository struct { 11 | habits []*Habit 12 | } 13 | 14 | // GetHabits retrieves the list of Habits. 15 | func (m MockHabitRepository) GetAllHabits() []*Habit { 16 | return m.habits 17 | } 18 | 19 | // Initialize mock database with sample data. 20 | func (m MockHabitRepository) NewHabits() *MockHabitRepository { 21 | return &MockHabitRepository{habits: []*Habit{ 22 | {ID: 1, Name: "yoga", Unit: "hours", Pomodoro: false, Why: "I want to do yoga so I can be more relaxed"}, 23 | {ID: 2, Name: "meditation", Unit: "15 minutes", Pomodoro: false, Why: "I want to meditate so I can be more present"}, 24 | {ID: 3, Name: "hydration", Unit: "litres", Pomodoro: false, Why: "I want to be hydrated so I can feel better"}, 25 | }, 26 | } 27 | } 28 | 29 | // findHabit finds a habit with a given ID. 30 | func (m MockHabitRepository) GetHabit(id uint) *Habit { 31 | for i := range m.habits { 32 | if m.habits[i].ID == id { 33 | return m.habits[i] 34 | } 35 | } 36 | return nil 37 | } 38 | 39 | // AddHabit adds a habit to Habits. 40 | // It returns true on successful completion. 41 | // Receives a JSON Habit object 42 | func (m MockHabitRepository) AddHabit(habit Habit) bool { 43 | m.habits = append(m.habits, &habit) 44 | return true 45 | } 46 | 47 | // EditHabit edits an existing habit in Habits 48 | func (m MockHabitRepository) EditHabit(req []byte) (*Habit, error) { 49 | var changes Habit 50 | json.Unmarshal(req, &changes) 51 | target := m.GetHabit(changes.ID) 52 | if target == nil { 53 | return nil, errors.Errorf("habit with id %d not found", changes.ID) 54 | } 55 | *target = changes 56 | return target, nil 57 | } 58 | 59 | // DeleteHabit deletes a habit from Habits by ID. 60 | // It returns the updated Habits 61 | func (m MockHabitRepository) DeleteHabit(id uint) []*Habit { 62 | for i := 0; i < len(m.habits); i++ { 63 | if m.habits[i].ID == id { 64 | fmt.Print(m.habits) 65 | m.habits = append(m.habits[:i], m.habits[i+1:]...) 66 | fmt.Print(m.habits) 67 | } 68 | } 69 | return m.habits 70 | } 71 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "habit_tracker", 3 | "description": "Enter your project description", 4 | "author": { 5 | "name": "bashbunni", 6 | "email": "bashbunni.io@gmail.com" 7 | }, 8 | "version": "0.1.0", 9 | "binaryname": "habit_tracker", 10 | "frontend": { 11 | "dir": "frontend", 12 | "install": "npm install", 13 | "build": "npm run build", 14 | "bridge": "src", 15 | "serve": "npm run serve" 16 | }, 17 | "tags": "", 18 | "WailsVersion": "", 19 | "CrossCompile": false, 20 | "Platform": "", 21 | "Architecture": "", 22 | "LdFlags": "", 23 | "GoPath": "", 24 | "UseFirebug": false 25 | } -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | _ "github.com/go-sql-driver/mysql" 10 | "github.com/joho/godotenv" 11 | ) 12 | 13 | func getEnvVariable(key string) string { 14 | err := godotenv.Load() 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | return os.Getenv(key) 19 | } 20 | 21 | func DBConnect() (db *sql.DB) { 22 | fmt.Println("Setup...") 23 | dbUsername := getEnvVariable("DB_USERNAME") 24 | dbPassword := getEnvVariable("DB_PASSWORD") 25 | dbHost := getEnvVariable("HOST") 26 | dbPort := getEnvVariable("PORT") 27 | dbName := getEnvVariable("DATABASE") 28 | db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUsername, dbPassword, dbHost, dbPort, dbName)) 29 | 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | return db 34 | } 35 | -------------------------------------------------------------------------------- /tests/habit_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestGetHabits(t *testing.T) { 6 | got := GetHabits() 7 | want := []Habit{ 8 | {id: 1, name: "yoga", unit: "hours", pomodoro: false, why: "I want to do yoga so I can be more relaxed"}, 9 | {id: 2, name: "meditation", unit: "15 minutes", pomodoro: false, why: "I want to meditate so I can be more present"}, 10 | {id: 3, name: "hydration", unit: "litres", pomodoro: false, why: "I want to be hydrated so I can feel better"}, 11 | } 12 | 13 | if got[0] != want[0] { 14 | t.Logf("got %v, want %v", got[0], want[0]) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | ) 7 | 8 | func JSONToHabit(req []byte) Habit { 9 | var h Habit 10 | err := json.Unmarshal([]byte(req), &h) 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | return h 15 | } 16 | 17 | func HabitToJSON(habit Habit) ([]byte, error) { 18 | return json.Marshal(habit) 19 | } 20 | 21 | func JSONToDate(req []byte) Date { 22 | var d Date 23 | err := json.Unmarshal([]byte(req), &d) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | return d 28 | } 29 | 30 | func DateToJSON(date Date) ([]byte, error) { 31 | return json.Marshal(date) 32 | } 33 | --------------------------------------------------------------------------------