Compare commits

..

59 Commits

Author SHA1 Message Date
479044d4ea Added quickLog™ 2023-07-29 20:41:17 +01:00
fd58f130a0 Add task blanks input fields, actually 2023-07-29 19:34:12 +01:00
4dcf065501 Add task blanks input fields 2023-07-29 19:28:16 +01:00
75413b2c5e Added 'add task' functionality 2023-07-29 19:25:37 +01:00
453f62cfae Displays 'such empty' if no task exists 2023-07-29 19:13:19 +01:00
fde246ecb7 Displays 'such empty' if no task has been completed 2023-07-29 19:11:50 +01:00
46cc0237ce added installation instructions 2023-07-22 21:42:40 +01:00
3506af77bf hid database for privacy 2023-07-22 21:40:52 +01:00
647e02d28e hid database for privacy 2023-07-22 21:38:27 +01:00
5bd5237bfa removed workflows 2023-07-22 21:38:04 +01:00
f829fb08d1 added test gitea action
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 20s
2023-07-22 21:06:25 +01:00
d77439bafd added test gitea action
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 1m27s
2023-07-22 20:53:34 +01:00
3110d315e6 added default activities 2023-07-22 11:22:06 +01:00
507f6c932b added default users 2023-07-22 11:20:24 +01:00
58eda6d1ea cleared db 2023-07-22 11:18:57 +01:00
e82adbe5e4 use port supplied to the program by tags 2023-07-22 11:11:20 +01:00
01b3b7b429 redundant variable 2023-07-22 11:04:02 +01:00
38f2f627a8 Tooltips shows if admin tries to get points 2023-07-22 11:03:28 +01:00
feef4c7800 Task buttons dont add points for admins 2023-07-22 11:01:36 +01:00
1d04006335 Stats card oh history screen 2023-07-22 10:58:14 +01:00
80e3d270c8 JS actually sends amount of points when completed 2023-07-22 10:57:52 +01:00
58b3dfe28a Did not update all SQL requests 2023-07-22 09:45:59 +01:00
758c87652c Adds points to user when action completed 2023-07-21 23:02:59 +01:00
0bdaac368c Finished route for getting user points 2023-07-21 22:53:32 +01:00
31913d0b94 Added route for getting the user points stats 2023-07-21 22:41:50 +01:00
fb9e3130e0 Rename types so they make sense 2023-07-21 22:40:16 +01:00
2aaa448331 Added points to the users table 2023-07-21 22:36:41 +01:00
fa8e4486f3 Added points to the users table 2023-07-21 22:33:14 +01:00
20da46a8d7 Added stat placeholder box 2023-07-21 22:27:42 +01:00
4a375149f6 Complete task javascript 2023-07-21 21:49:55 +01:00
b02dfc0dac Tooltip for the + Task button while it does nothing 2023-07-21 21:49:28 +01:00
3e77074bec Tooltip for the + Task button while it does nothing 2023-07-21 21:49:16 +01:00
bf8594a546 Scrolling of content when it overflows 2023-07-21 21:48:48 +01:00
19110e74b4 Bug fix: Login screen be scrolling 2023-07-21 18:57:17 +01:00
840d6e2ff5 Javascript not understanding how arrays work 2023-07-21 18:50:46 +01:00
6ad1e9aa5c Fixed error caused by canceling the SQL statement before it is used 2023-07-21 18:37:40 +01:00
5d2aba9caa Removed pointless error checking 2023-07-21 18:36:34 +01:00
d5cae327f3 Removed err check halting program 2023-07-21 18:26:53 +01:00
02b69b8cdf Updated history view api to return more human-readable data 2023-07-21 18:25:57 +01:00
8b1d66617f Basic task and history view 2023-07-21 18:09:02 +01:00
b26b0c01b0 Changed add to + for better readability on ios 2023-07-21 17:35:26 +01:00
f8b9b5407b Improved styling on buttons 2023-07-21 17:27:08 +01:00
31a96558bf More descriptive login screen 2023-07-21 17:26:38 +01:00
bba55bfc0e Custom item styling 2023-07-21 17:26:13 +01:00
cfb643832b Added the db to git 2023-07-21 17:25:19 +01:00
497c916d93 Removed the database from gitignore 2023-07-21 17:25:06 +01:00
8c07677a01 Display different buttons based on role 2023-07-21 15:45:12 +01:00
677ac0241d Basic screen switching 2023-07-21 15:33:45 +01:00
d014848e71 Updated packages 2023-07-21 15:06:52 +01:00
05d1952c84 Added release mode toggle 2023-07-21 15:06:29 +01:00
fae7507407 Added release mode toggle 2023-07-21 15:06:17 +01:00
f24dcae19f Started main page ui 2023-07-17 20:14:00 +01:00
3761fc1b1e Fixed sql error 2023-07-17 17:42:33 +01:00
b704db1ba1 Login page functionality 2023-07-17 17:42:12 +01:00
b3d846c6f4 Styled login ui 2023-07-17 17:10:16 +01:00
f5f19f6dd4 Basic frontend login ui 2023-07-17 16:52:39 +01:00
af171b3344 index.html 2023-07-16 19:46:52 +01:00
3b9df825cd Started frontend 2023-07-16 18:44:56 +01:00
eb62c78624 Started frontend 2023-07-16 18:43:58 +01:00
9 changed files with 464 additions and 28 deletions

2
.gitignore vendored
View File

@@ -126,7 +126,6 @@ dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
@@ -155,7 +154,6 @@ dist
# custom
# database
database/data.db
#

View File

@@ -1,11 +1,8 @@
# GiacPoints
A system for keeping track of house chores completed and attributing points to the activities.
# Functionality
Webpage - Login, add chores/activities (admin), tick of chores (user), log of completed chores (all)
API layer - /loginUser, /getTasks, /completeTask, /getHistory?filter, /addTask
Backend - DB, user (name password userid role), activities (taskid name points), history (userid taskid time pointsGained)
# Installation
`Note: Linux only`
1. Clone this repo
2. rename `database/example.db` to `database/data.db`
3. Execute `main`

BIN
database/example.db Normal file

Binary file not shown.

67
frontend/index.css Normal file
View File

@@ -0,0 +1,67 @@
.flex {
margin-left: 0;
}
.loginSpacer {
height: 33%;
}
#login {
width: 100%;
height: 100%;
}
#content {
height: 85%;
width: 100%;
padding: 5%;
}
#menu {
height: auto;
width: 100%;
padding: 5%;
}
#mainPage {
width: 100%;
height: 100%;
}
.screen {
width: 95%;
height: 100%;
}
.button {
width: 100%;
}
#taskButton {
padding-left: 0;
}
#historyContent {
height: 75%;
overflow: scroll;
width: 100%;
}
#stats {
height: 25%;
width: 100%;
}
#tasks {
overflow: scroll;
}
pre {
padding: 0;
background: white;
border-radius: 0;
}
#taskCard {
margin-top: 45%;
}

70
frontend/index.html Normal file
View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GiacPoints</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/picnic">
<link rel="stylesheet" href="/frontend/index.css">
</head>
<body>
<div id="login">
<div class="flex three demo" style="height: 100%; width: 100%">
<div class="fourth two-fifth-1000"></div>
<div class="flex one demo half fifth-1000" style="height: 100%">
<div class="loginSpacer"></div>
<article class="card" style="padding: 5%">
<label for="name">Name</label> <br>
<input type="text" id="name"> <br>
<label for="password">Password</label> <br>
<input type="password" id="password"> <br>
<input type="submit" value="Login" style="width: 100%" onclick="login()" id="loginSubmit"> <br>
</article>
<div class="loginSpacer"></div>
</div>
<div class="fourth two-fifth-1000"></div>
</div>
</div>
<div id="mainPage" style="display: none;">
<div id="content">
<div id="tasks" class="screen flex two demo" style=""></div>
<div id="history" class="screen" style="display: none;">
<div id="stats">
<article class="card">
<header>
<h1>Stats</h1>
<pre id="statData"></pre>
</header>
</article>
</div>
<div id="historyContent"></div>
</div>
<div id="addTask" class="screen" style="display: none;">
<article class="card" id="taskCard">
<header>
<h1>New task</h1> <br>
<label for="newTaskName">Task name</label>
<input type="text" id="newTaskName"> <br>
<label for="newTaskPoints">Task points</label>
<input type="number" id="newTaskPoints" min="1" max="5"> <br>
<input onclick="newTask()" value="Create" type="submit"> <br>
</header>
</article>
</div>
</div>
<div id="menu" class="flex three demo">
<div class="third" id="taskButton">
<span class="button" onclick="switchScreen('task')">Tasks</span>
</div>
<div class="third" id="historyButton">
<span class="button" onclick="switchScreen('history')">History</span>
</div>
<div class="third" id="addButton">
<span class="button tooltip-top" onclick="switchScreen('add')" >+ Task</span>
<!-- <span class="button tooltip-top" disabled data-tooltip="In construction">+ Task</span>-->
</div>
</div>
</div>
<script src="/frontend/index.js"></script>
</body>
</html>

200
frontend/index.js Normal file
View File

@@ -0,0 +1,200 @@
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function login() {
// tell the user the app is loading
document.getElementById("loginSubmit").value = "Loading";
// this.preventDefault();
// get login details
let name = document.getElementById("name").value;
let password = document.getElementById("password").value;
// request login
let response = await fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: name,
password: password
})
});
// if failed red button
// else switch screen
if (response.status !== 200) {
document.getElementById("loginSubmit").classList.add("error");
document.getElementById("loginSubmit").value = "Failed";
return;
} else {
// switch screen
document.getElementById("login").setAttribute("style", "display: none;");
document.getElementById("mainPage").setAttribute("style", "");
}
// store user data in session storage
let data = await response.json();
localStorage.setItem("uid", data.uid);
localStorage.setItem("name", data.name);
localStorage.setItem("role", data.role);
// if role is admin, keep auto styling
if (localStorage.getItem("role") !== "admin") {
document.getElementById("addButton").setAttribute("style", "display: none;");
document.getElementById("taskButton").classList.value = "half";
document.getElementById("historyButton").classList.value = "half";
document.getElementById("menu").classList.value = "flex two";
}
await populateScreen();
}
async function populateScreen() {
// get task data
let taskPage = document.getElementById("tasks");
taskPage.innerHTML = "";
let response = await fetch("/getTasks");
let tasksRaw = await response.json();
let tasks = tasksRaw.tasks;
// get historical data
let historyPage = document.getElementById("historyContent");
historyPage.innerHTML = "";
response = await fetch("/getHistory");
let historyRaw = await response.json();
let history = historyRaw.history;
// get points data
let statsPage = document.getElementById("statData");
statsPage.innerText = "";
response = await fetch("/getUserPoints");
let statsRaw = await response.json();
let stats = statsRaw.userPoints;
// if it is a user make the tasks add points
// or if admin make it do nothing
if (localStorage.getItem("role") === "user") {
try {
for (let i = 0; i < tasks.length; i++) {
taskPage.innerHTML += `
<div id="${tasks[i].tid}" class="half" style="height: fit-content">
<div class="button" onclick="completeTask(${tasks[i].tid})">
<p id="taskName">${tasks[i].name}</p>
<p id="${tasks[i].tid}-p">${tasks[i].points} points</p>
</div>
</div>`
}
} catch (e) {
taskPage.innerHTML +=`<sub>such empty</sub>`
}
} else if (localStorage.getItem("role") === "admin") {
try {
for (let i = 0; i < tasks.length; i++) {
taskPage.innerHTML += `
<div id="${tasks[i].tid}" class="half" style="height: fit-content">
<div class="button" disabled data-tooltip="Admins can't get points">
<p id="taskName">${tasks[i].name}</p>
<p id="${tasks[i].tid}-p">${tasks[i].points} points</p>
</div>
</div>`
}
} catch (e) {
taskPage.innerHTML +=`<sub>such empty</sub>`
}
}
try {
for (let i = (history.length -1); i >= 0; i-=1) {
historyPage.innerHTML += `
<div class="full">
<article class="card">
<header>
<p>User: ${history[i].user}</p>
<p>Task: ${history[i].task}</p>
<p>Time: ${history[i].time}</p>
<p>Points gained: ${history[i].pointsGained}</p>
</header>
</article>
</div>`
}
} catch (e) {
historyPage.innerHTML += `
<sub>Such empty</sub>`
}
for (let i = 0; i < stats.length; i++) {
// put user points in box
statsPage.innerText += `${stats[i].name}: ${stats[i].points} \n`
}
}
async function completeTask(taskID) {
let points = document.getElementById(`${taskID}-p`).innerText.split(" ")[0];
let time = new Date().toISOString().split(".")[0];
let uid = localStorage.getItem("uid");
await fetch("/completeTask", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
uid: Number(uid),
tid: Number(taskID),
time: time,
pointsGained: Number(points)
})
});
}
async function switchScreen(button) {
await populateScreen();
document.getElementById("tasks").setAttribute("style", "display: none;");
document.getElementById("history").setAttribute("style", "display: none;");
document.getElementById("addTask").setAttribute("style", "display: none;");
if (button === "task") {
document.getElementById("tasks").setAttribute("style", "");
} else if (button === "history") {
document.getElementById("history").setAttribute("style", "");
} else if (button === "add") {
document.getElementById("addTask").setAttribute("style", "");
}
}
async function newTask() {
let points = document.getElementById(`newTaskPoints`);
let name = document.getElementById(`newTaskName`);
await fetch("/addTask", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
tid: 0,
name: name.value,
points: Number(points.value),
})
});
points.value = "";
name.value = "";
}
async function fastLog() {
sleep(500);
if (localStorage.getItem("uid") != null && localStorage.getItem("name") != null && localStorage.getItem("role") != null) {
document.getElementById("login").setAttribute("style", "display: none;");
document.getElementById("mainPage").setAttribute("style", "");
if (localStorage.getItem("role") !== "admin") {
document.getElementById("addButton").setAttribute("style", "display: none;");
document.getElementById("taskButton").classList.value = "half";
document.getElementById("historyButton").classList.value = "half";
document.getElementById("menu").classList.value = "flex two";
}
await populateScreen();
} else {
// do nothing
}
}
fastLog();

6
go.mod
View File

@@ -2,7 +2,10 @@ module giacPoints
go 1.20
require github.com/gin-gonic/gin v1.9.1
require (
github.com/gin-gonic/gin v1.9.1
github.com/mattn/go-sqlite3 v1.14.17
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
@@ -19,7 +22,6 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect

BIN
main

Binary file not shown.

132
main.go
View File

@@ -2,6 +2,7 @@ package main
import (
"database/sql"
"flag"
"github.com/gin-gonic/gin"
_ "github.com/mattn/go-sqlite3"
"net/http"
@@ -17,11 +18,12 @@ type loginInput struct {
Password string `json:"password"`
}
type loginOutput struct {
type userData struct {
UID int `json:"uid"`
Name string `json:"name"`
Password string `json:"password"`
Role string `json:"role"`
Points int `json:"points"`
}
type task struct {
@@ -34,39 +36,57 @@ type taskArray struct {
Tasks []task `json:"tasks"`
}
type historyData struct {
type newHistoryData struct {
UID int `json:"uid"`
TID int `json:"tid"`
Time string `json:"time"`
PointsGained int `json:"pointsGained"`
}
type historyData struct {
User string `json:"user"`
Task string `json:"task"`
Time string `json:"time"`
PointsGained int `json:"pointsGained"`
}
type historyArray struct {
History []historyData `json:"history"`
}
type userPointsData struct {
Name string `json:"name"`
Points int `json:"points"`
}
type userPointsArray struct {
UserPoints []userPointsData `json:"userPoints"`
}
// log the user into their account
func login(c *gin.Context) {
// get the username and password from the request
var userData loginInput
if err := c.BindJSON(&userData); err != nil {
c.IndentedJSON(http.StatusBadRequest, userData)
var requestedUser loginInput
if err := c.BindJSON(&requestedUser); err != nil {
c.IndentedJSON(http.StatusBadRequest, requestedUser)
return
}
// check for user using given credentials
stmt, err := db.Prepare("SELECT * FROM users WHERE name=?")
checkErr(err)
defer stmt.Close()
var user loginOutput
err = stmt.QueryRow(userData.Name).Scan(&user.Name, &user.Password, &user.UID, &user.Role)
var user userData
err = stmt.QueryRow(requestedUser.Name).Scan(&user.UID, &user.Name, &user.Password, &user.Role, &user.Points)
if err != nil {
// search failed user not real
c.IndentedJSON(http.StatusNotFound, userData)
c.IndentedJSON(http.StatusNotFound, requestedUser)
panic(err)
return
}
if user.Password != userData.Password {
if user.Password != requestedUser.Password {
// user not real
c.IndentedJSON(http.StatusNotFound, userData)
c.IndentedJSON(http.StatusNotFound, requestedUser)
panic(err)
return
} else {
// user is in
@@ -98,7 +118,7 @@ func getTasks(c *gin.Context) {
func completeTask(c *gin.Context) {
// get the task data from the request
var completedTask historyData
var completedTask newHistoryData
if err := c.BindJSON(&completedTask); err != nil {
c.IndentedJSON(http.StatusBadRequest, completedTask)
return
@@ -114,17 +134,30 @@ func completeTask(c *gin.Context) {
c.IndentedJSON(http.StatusNotModified, completedTask)
return
}
// add points to user table
stmt, err = db.Prepare("UPDATE users SET points = points + ? WHERE uid=?")
if err != nil {
c.IndentedJSON(http.StatusNotModified, completedTask)
return
}
_, err = stmt.Exec(completedTask.PointsGained, completedTask.UID)
if err != nil {
c.IndentedJSON(http.StatusNotModified, completedTask)
return
}
c.IndentedJSON(http.StatusOK, completedTask)
}
func getHistory(c *gin.Context) {
// get the log of past points gained
var history []historyData
var history []newHistoryData
// get array of all history Data
rows, err := db.Query("SELECT * FROM history")
checkErr(err)
for rows.Next() {
var tempHistoryData historyData
var tempHistoryData newHistoryData
err = rows.Scan(&tempHistoryData.UID, &tempHistoryData.TID, &tempHistoryData.Time, &tempHistoryData.PointsGained)
if err != nil {
c.IndentedJSON(http.StatusNotFound, history)
@@ -133,8 +166,37 @@ func getHistory(c *gin.Context) {
history = append(history, tempHistoryData)
}
rows.Close()
var historyRes []historyData
// make the data human-readable
for i := 0; i < len(history); i++ {
var tempHistory historyData
// get the username
var tempUser userData
stmt, err := db.Prepare("SELECT * FROM users WHERE uid=?")
checkErr(err)
err = stmt.QueryRow(history[i].UID).Scan(&tempUser.UID, &tempUser.Name, &tempUser.Password, &tempUser.Role, &tempUser.Points)
checkErr(err)
tempHistory.User = tempUser.Name
stmt.Close()
// get the task name and points
var tempTask task
stmt, err = db.Prepare("SELECT * FROM activities WHERE taskId=?")
checkErr(err)
err = stmt.QueryRow(history[i].TID).Scan(&tempTask.TID, &tempTask.Name, &tempTask.Points)
checkErr(err)
tempHistory.Task = tempTask.Name
tempHistory.PointsGained = tempTask.Points
stmt.Close()
tempHistory.Time = history[i].Time
historyRes = append(historyRes, tempHistory)
}
var jsonHistory historyArray
jsonHistory.History = history
jsonHistory.History = historyRes
c.IndentedJSON(http.StatusOK, jsonHistory)
}
@@ -159,15 +221,55 @@ func addTask(c *gin.Context) {
c.IndentedJSON(http.StatusOK, newTask)
}
func getUserPoints(c *gin.Context) {
// get the names of all users
var allUsers []userPointsData
rows, err := db.Query("SELECT name, points FROM users WHERE role='user'")
checkErr(err)
// put them in an array of users
for rows.Next() {
var tempUser userPointsData
err = rows.Scan(&tempUser.Name, &tempUser.Points)
if err != nil {
c.IndentedJSON(http.StatusNotFound, allUsers)
return
}
allUsers = append(allUsers, tempUser)
}
rows.Close()
// return to the requester
var jsonUserPoints userPointsArray
jsonUserPoints.UserPoints = allUsers
c.IndentedJSON(http.StatusOK, jsonUserPoints)
}
func main() {
var port string
flag.StringVar(&port, "port", "localhost:8080", "define the port you want the program to use to serve")
flag.Parse()
//release mode
//gin.SetMode(gin.ReleaseMode)
router := gin.Default()
// api routes
router.POST("/login", login)
router.POST("/completeTask", completeTask)
router.POST("/addTask", addTask)
router.GET("/getTasks", getTasks)
router.GET("/getHistory", getHistory)
router.GET("/getUserPoints", getUserPoints)
router.Run("localhost:8080")
// page routes
router.LoadHTMLGlob("frontend/*")
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
})
router.Static("/frontend", "./frontend")
router.Run(port)
}
func checkErr(err error) {