├── .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 | 
2 |
3 | 
4 | 
5 | 
6 | 
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 | 
15 | 
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 |
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 | 
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 | You need to enable JavaScript to run this app.
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 |
40 |
41 | {nav && }
42 |
43 |
44 |
45 | (
49 |
50 | )}
51 | />
52 | (
55 |
56 | )}
57 | />
58 | (
62 |
63 | )}
64 | />
65 |
66 |
67 |
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 |
{
35 | setAddOpen(false);
36 | }}
37 | />
38 |
Log Activity
39 |
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 |
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 |
{
42 | setEditOpen(false);
43 | }}
44 | />
45 |
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 |
15 |
16 | {habitList.map((habit) => (
17 |
18 |
19 | {habit.name}
20 |
21 |
22 | ))}
23 |
24 |
25 | pomodoro
26 |
27 |
28 |
29 |
30 | add new
31 |
32 |
33 |
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 |
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 |
{
80 | setEditOpen(true);
81 | }}
82 | />
83 |
{
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 |
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 |
--------------------------------------------------------------------------------