Repository: jonbertelsen/lifehackspring2025
Pull request #40: Lifehack team 12 developer branch
Author: login: Mikkel-ao
Created: 2025-03-21T12:18:26Z | Merged: 2025-03-22T22:02:52Z

Updated pull request. Picture for thumbnail in 200x200 format is found in the team12/images folder. Exported database in team12/sql folder.


Diff:
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..440153d
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,15 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="myValues">
+        <value>
+          <list size="1">
+            <item index="0" class="java.lang.String" itemvalue="href" />
+          </list>
+        </value>
+      </option>
+      <option name="myCustomValuesEnabled" value="true" />
+    </inspection_tool>
+  </profile>
+</component>
\ No newline at end of file
diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java
index dd464fe..ed7a305 100644
--- a/src/main/java/app/Main.java
+++ b/src/main/java/app/Main.java
@@ -3,9 +3,12 @@
 import app.config.SessionConfig;
 import app.config.ThymeleafConfig;
 
+
+
 import app.controllers.Team2Controller;
 import app.controllers.Team7Controller;
 import app.controllers.Team10Controller;
+import app.controllers.Team12Controller;
 import app.controllers.TeamTeacherController;
 
 import app.persistence.ConnectionPool;
@@ -24,6 +27,7 @@ public class Main {
 
     private static final ConnectionPool connectionPool = ConnectionPool.getInstance(USER, PASSWORD, URL, DB);
 
+
     public static void main(String[] args) {
 
         // Initializing Javalin and Jetty webserver
@@ -34,7 +38,6 @@ public static void main(String[] args) {
             config.fileRenderer(new JavalinThymeleaf(ThymeleafConfig.templateEngine()));
         }).start(7070);
 
-
       
         // Frontpage
         app.get("/", ctx ->  ctx.render("index.html"));
@@ -54,6 +57,8 @@ public static void main(String[] args) {
         // Team10
         Team10Controller.routes(app, connectionPool);
 
+        // Team12
+        Team12Controller.addRoutes(app, connectionPool);
     }
 
 }
\ No newline at end of file
diff --git a/src/main/java/app/controllers/Team12Controller.java b/src/main/java/app/controllers/Team12Controller.java
new file mode 100644
index 0000000..4787e5a
--- /dev/null
+++ b/src/main/java/app/controllers/Team12Controller.java
@@ -0,0 +1,115 @@
+package app.controllers;
+
+import app.entitites.Team12SleepRecords;
+import app.entitites.Team12User;
+import app.exceptions.Team12DatabaseException;
+import app.persistence.ConnectionPool;
+import app.persistence.Team12SleepMapper;
+import app.persistence.Team12UserMapper;
+import io.javalin.Javalin;
+import io.javalin.http.Context;
+
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+
+public class Team12Controller {
+
+    public static void addRoutes(Javalin app, ConnectionPool connectionPool) {
+        // app.get("/", ctx -> ctx.render("/team12/team12_index.html"));
+        app.get("/team12", ctx -> ctx.render("/team12/team12_index.html"));
+        app.post("/team12/login", ctx -> login(ctx, connectionPool));
+        app.get("/team12/tracker", ctx -> ctx.render("/team12/team12_tracker.html"));
+        app.get("/team12/logout", Team12Controller::logout);
+        app.get("/team12/createuser", ctx -> ctx.render("/team12/team12_createuser.html"));
+        app.post("/team12/createuser", ctx -> createUser(ctx, connectionPool));
+        app.post("/team12/calculate", ctx -> sleep(ctx, connectionPool));
+        app.get("/team12/dashboard", ctx -> ctx.render("/team12/team12_dashboard.html"));
+        app.get("/team12/sleep-data", ctx -> fetchSleepData(ctx, connectionPool));
+
+    }
+
+    private static void createUser(Context ctx, ConnectionPool connectionPool) {
+        String username = ctx.formParam("username");
+        String password1 = ctx.formParam("password1");
+        String password2 = ctx.formParam("password2");
+
+        if (!password1.equals(password2)) {
+            ctx.attribute("message", "Passwords do not match, try again.");
+            ctx.render("/team12/team12_createuser.html");
+            return;
+        }
+
+        try {
+            Team12UserMapper.createUser(username, password1, connectionPool);
+            ctx.attribute("message", "User created successfully. Please log in.");
+            ctx.render("/team12/team12_index.html");
+        } catch (Team12DatabaseException e) {
+            ctx.attribute("message", "User already exists. Try again or log in.");
+            ctx.render("/team12/team12_createuser.html");
+        }
+    }
+
+    private static void logout(Context ctx) {
+        ctx.req().getSession().invalidate();
+        ctx.redirect("/team12");
+    }
+
+    private static void login(Context ctx, ConnectionPool connectionPool) {
+        String username = ctx.formParam("username");
+        String password = ctx.formParam("password");
+
+        try {
+            Team12User user = Team12UserMapper.login(username, password, connectionPool);
+
+            if (user == null) {
+                ctx.attribute("message", "Invalid username or password.");
+                ctx.render("/team12/team12_index.html");
+                return;
+            }
+
+            ctx.sessionAttribute("currentUser", user);
+            ctx.redirect("/team12/tracker");
+        } catch (Team12DatabaseException e) {
+            ctx.attribute("message", "Login failed. Please try again.");
+            ctx.render("/team12/team12_index.html");
+        }
+    }
+
+    private static void sleep(Context ctx, ConnectionPool connectionPool) {
+        Team12User currentUser = ctx.sessionAttribute("currentUser");
+        if (currentUser == null) {
+            ctx.attribute("message", "User not logged in.");
+            ctx.render("/team12/team12_tracker.html");
+            return;
+        }
+
+        try {
+            Timestamp sleepStart = Timestamp.valueOf(ctx.formParam("sleep_start").replace('T', ' ') + ":00");
+            Timestamp sleepEnd = Timestamp.valueOf(ctx.formParam("sleep_end").replace('T', ' ') + ":00");
+
+            Team12SleepMapper.saveSleepData(currentUser.getUserId(), sleepStart, sleepEnd, connectionPool);
+            ctx.attribute("message", "Sleep data recorded.");
+        } catch (Team12DatabaseException e) {
+            ctx.attribute("message", "Failed to record sleep data.");
+        } catch (IllegalArgumentException e) {
+            ctx.attribute("message", "Invalid date format.");
+        }
+        ctx.render("/team12/team12_tracker.html");
+    }
+
+    private static void fetchSleepData(Context ctx, ConnectionPool connectionPool) {
+        Team12User currentUser = ctx.sessionAttribute("currentUser");
+        if (currentUser == null) {
+            ctx.status(401).json(Map.of("error", "User not logged in"));
+            return;
+        }
+
+        try {
+            List<Team12SleepRecords> sleepRecords = Team12SleepMapper.getSleepDataByUserId(currentUser.getUserId(), connectionPool);
+            ctx.json(sleepRecords);
+        } catch (Team12DatabaseException e) {
+            ctx.status(500).json(Map.of("error", "Failed to fetch sleep data"));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/app/entitites/Team12SleepAnalytics.java b/src/main/java/app/entitites/Team12SleepAnalytics.java
new file mode 100644
index 0000000..31d0c7e
--- /dev/null
+++ b/src/main/java/app/entitites/Team12SleepAnalytics.java
@@ -0,0 +1,61 @@
+package app.entitites;
+
+import java.security.Timestamp;
+import java.time.LocalDateTime;
+import java.util.Date;
+
+public class Team12SleepAnalytics {
+    private int analyticsId;
+    private int userId;
+    private Date sleepStartDate;
+    private double totalSleep;
+    private String lastUpdated;
+
+    public Team12SleepAnalytics(int analyticsId, int userId, Date sleepStartDate, double totalSleep, String lastUpdated) {
+        this.analyticsId = analyticsId;
+        this.userId = userId;
+        this.sleepStartDate = sleepStartDate;
+        this.totalSleep = totalSleep;
+        this.lastUpdated = lastUpdated;
+    }
+
+    public int getAnalyticsId() {
+        return analyticsId;
+    }
+
+    public void setAnalyticsId(int analyticsId) {
+        this.analyticsId = analyticsId;
+    }
+
+    public int getUserId() {
+        return userId;
+    }
+
+    public void setUserId(int userId) {
+        this.userId = userId;
+    }
+
+    public Date getSleepStartDate() {
+        return sleepStartDate;
+    }
+
+    public void setSleepStartDate(Date sleepStartDate) {
+        this.sleepStartDate = sleepStartDate;
+    }
+
+    public double getTotalSleep() {
+        return totalSleep;
+    }
+
+    public void setTotalSleep(double totalSleep) {
+        this.totalSleep = totalSleep;
+    }
+
+    public String getLastUpdated() {
+        return lastUpdated;
+    }
+
+    public void setLastUpdated(String lastUpdated) {
+        this.lastUpdated = lastUpdated;
+    }
+}
diff --git a/src/main/java/app/entitites/Team12SleepRecords.java b/src/main/java/app/entitites/Team12SleepRecords.java
new file mode 100644
index 0000000..73aaee8
--- /dev/null
+++ b/src/main/java/app/entitites/Team12SleepRecords.java
@@ -0,0 +1,70 @@
+package app.entitites;
+
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+// Trying out pull requests comment to apply change
+public class Team12SleepRecords {
+    private int recordId;
+    private int userId;
+    private Timestamp sleepStart;
+    private Timestamp sleepEnd;
+    private double sleepDuration;
+    private String createAt;
+
+    public Team12SleepRecords(int recordId, int userId, Timestamp sleepStart, Timestamp sleepEnd, double sleepDuration, String createAt) {
+        this.recordId = recordId;
+        this.userId = userId;
+        this.sleepStart = sleepStart;
+        this.sleepEnd = sleepEnd;
+        this.sleepDuration = sleepDuration;
+        this.createAt = createAt;
+    }
+
+    public int getRecordId() {
+        return recordId;
+    }
+
+    public void setRecordId(int recordId) {
+        this.recordId = recordId;
+    }
+
+    public int getUserId() {
+        return userId;
+    }
+
+    public void setUserId(int userId) {
+        this.userId = userId;
+    }
+
+    public Timestamp getSleepStart() {
+        return sleepStart;
+    }
+
+    public void setSleepStart(Timestamp sleepStart) {
+        this.sleepStart = sleepStart;
+    }
+
+    public Timestamp getSleepEnd() {
+        return sleepEnd;
+    }
+
+    public void setSleepEnd(Timestamp sleepEnd) {
+        this.sleepEnd = sleepEnd;
+    }
+
+    public double getSleepDuration() {
+        return sleepDuration;
+    }
+
+    public void setSleepDuration(double sleepDuration) {
+        this.sleepDuration = sleepDuration;
+    }
+
+    public String getCreateAt() {
+        return createAt;
+    }
+
+    public void setCreateAt(String createAt) {
+        this.createAt = createAt;
+    }
+}
diff --git a/src/main/java/app/entitites/Team12User.java b/src/main/java/app/entitites/Team12User.java
new file mode 100644
index 0000000..c04db49
--- /dev/null
+++ b/src/main/java/app/entitites/Team12User.java
@@ -0,0 +1,60 @@
+package app.entitites;
+
+import java.time.LocalDateTime;
+
+public class Team12User {
+    private int userId;
+    private static String username;
+    private String password;
+    private LocalDateTime createdAt;
+
+    public Team12User(int userId, String username, String password, LocalDateTime createdAt) {
+        this.userId = userId;
+        this.username = username;
+        this.password = password;
+        this.createdAt = createdAt;
+    }
+
+    public int getUserId() {
+        return userId;
+    }
+
+    public void setUserId(int userId) {
+        this.userId = userId;
+    }
+
+    public static String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public LocalDateTime getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(LocalDateTime createdAt) {
+        this.createdAt = createdAt;
+    }
+
+
+    @Override
+    public String toString() {
+        return "Team12User{" +
+                "user_id=" + userId +
+                ", username='" + username + '\'' +
+                ", password='" + password + '\'' +
+                ", createdAt=" + createdAt +
+                '}';
+    }
+}
diff --git a/src/main/java/app/exceptions/Team12DatabaseException.java b/src/main/java/app/exceptions/Team12DatabaseException.java
new file mode 100644
index 0000000..0488429
--- /dev/null
+++ b/src/main/java/app/exceptions/Team12DatabaseException.java
@@ -0,0 +1,15 @@
+package app.exceptions;
+
+public class Team12DatabaseException extends Throwable {
+
+    public Team12DatabaseException(String userMessage) {
+        super(userMessage);
+        System.out.println("userMessage: " + userMessage);
+    }
+
+    public Team12DatabaseException(String userMessage, String systemMessage) {
+        super(userMessage);
+        System.out.println("userMessage: " + userMessage);
+        System.out.println("errorMessage: " + systemMessage);
+    }
+}
diff --git a/src/main/java/app/persistence/Team12SleepMapper.java b/src/main/java/app/persistence/Team12SleepMapper.java
new file mode 100644
index 0000000..15ac376
--- /dev/null
+++ b/src/main/java/app/persistence/Team12SleepMapper.java
@@ -0,0 +1,72 @@
+package app.persistence;
+
+import app.entitites.Team12SleepRecords;
+import app.exceptions.Team12DatabaseException;
+
+import java.sql.*;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Team12SleepMapper {
+
+    // Saves sleep data (user ID, start time, end time) to the database
+    public static void saveSleepData(int userId, Timestamp sleepStart, Timestamp sleepEnd, ConnectionPool connectionPool) throws Team12DatabaseException {
+        String sql = "INSERT INTO team12_sleep_records (user_id, sleep_start, sleep_end) VALUES (?, ?, ?)";
+
+        try (
+                Connection connection = connectionPool.getConnection();
+                PreparedStatement ps = connection.prepareStatement(sql)
+        ) {
+            ps.setInt(1, userId);
+            ps.setTimestamp(2, sleepStart);
+            ps.setTimestamp(3, sleepEnd);
+
+            int rowsAffected = ps.executeUpdate();
+            if (rowsAffected != 1) {
+                throw new Team12DatabaseException("Failed to insert sleep data.");
+            }
+        } catch (SQLException e) {
+            String msg = "An error occurred. Please try again.";
+            if (e.getMessage().startsWith("ERROR: duplicate key value ")) {
+                msg = "Duplicate entry error.";
+            }
+            System.out.println("Error in database: " + e.getMessage());
+            throw new Team12DatabaseException(msg, e.getMessage());
+        }
+    }
+
+    public static List<Team12SleepRecords> getSleepDataByUserId(int userId, ConnectionPool connectionPool) throws Team12DatabaseException {
+        String sql = "SELECT record_id, user_id, sleep_start, sleep_end, sleep_duration, created_at FROM team12_sleep_records WHERE user_id = ?";
+        List<Team12SleepRecords> sleepRecords = new ArrayList<>();
+
+        try (Connection connection = connectionPool.getConnection();
+             PreparedStatement ps = connection.prepareStatement(sql)) {
+
+            ps.setInt(1, userId);
+            ResultSet rs = ps.executeQuery();
+
+            if (!rs.isBeforeFirst()) {
+                System.out.println("No sleep records found for user ID: " + userId);
+            }
+
+            while (rs.next()) {
+                Team12SleepRecords record = new Team12SleepRecords(
+                        rs.getInt("record_id"),
+                        rs.getInt("user_id"),
+                        rs.getTimestamp("sleep_start"),
+                        rs.getTimestamp("sleep_end"),
+                        rs.getDouble("sleep_duration"),
+                        rs.getString("created_at")  // Use the correct column name here
+                );
+                sleepRecords.add(record);
+            }
+        } catch (SQLException e) {
+            System.out.println("Error fetching sleep data for user ID: " + userId);
+            System.out.println("Error in database: " + e.getMessage());
+            throw new Team12DatabaseException("Error fetching sleep data for user ID: " + userId);
+        }
+
+        return sleepRecords;
+    }
+
+}
diff --git a/src/main/java/app/persistence/Team12UserMapper.java b/src/main/java/app/persistence/Team12UserMapper.java
new file mode 100644
index 0000000..f92fe74
--- /dev/null
+++ b/src/main/java/app/persistence/Team12UserMapper.java
@@ -0,0 +1,63 @@
+package app.persistence;
+
+import app.entitites.Team12User;
+import app.exceptions.Team12DatabaseException;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.LocalDateTime;
+
+public class Team12UserMapper {
+
+        // Authenticates the user by verifying username and password from the database
+    public static Team12User login(String username, String password, ConnectionPool connectionPool) throws Team12DatabaseException {
+        String sql = "select * from team12_users where username=? and password_hash=?";
+
+        try (
+                Connection connection = connectionPool.getConnection();
+                PreparedStatement ps = connection.prepareStatement(sql)
+        ) {
+            ps.setString(1, username);
+            ps.setString(2, password);
+
+            ResultSet rs = ps.executeQuery();
+            if (rs.next()) {
+                int id = rs.getInt("user_id");
+                LocalDateTime createdAt = rs.getTimestamp("created_at").toLocalDateTime();
+                return new Team12User(id, username, password, createdAt);
+            } else {
+                throw new Team12DatabaseException("Fejl i login. Prøv igen");
+            }
+        } catch (SQLException e) {
+            throw new Team12DatabaseException("DB fejl", e.getMessage());
+        }
+    }
+    // Creates a new user by inserting username and password into the database
+    public static void createUser(String username, String password, ConnectionPool connectionPool) throws Team12DatabaseException {
+        String sql = "insert into team12_users (username, password_hash) values (?,?)";
+
+        try (
+                Connection connection = connectionPool.getConnection();
+                PreparedStatement ps = connection.prepareStatement(sql)
+        ) {
+            ps.setString(1, username);
+            ps.setString(2, password);
+
+            int rowsAffected = ps.executeUpdate();
+            if (rowsAffected != 1) {
+                throw new Team12DatabaseException("Fejl ved oprettelse af ny bruger");
+            }
+        } catch (SQLException e) {
+            String msg = "Der er sket en fejl. Prøv igen";
+            if (e.getMessage().startsWith("ERROR: duplicate key value ")) {
+                msg = "Brugernavnet findes allerede. Vælg et andet";
+            }
+            throw new Team12DatabaseException(msg, e.getMessage());
+        }
+    }
+
+
+}
+
diff --git a/src/main/resources/public/team12/css/team12_styles.css b/src/main/resources/public/team12/css/team12_styles.css
new file mode 100644
index 0000000..53f4d12
--- /dev/null
+++ b/src/main/resources/public/team12/css/team12_styles.css
@@ -0,0 +1,198 @@
+/* Stylesheet for the application */
+
+h1, h2, h3, h4, h5, h6, p {
+    color: #c3c3c3;
+}
+
+/* For editing purposes, delete when done
+
+header {
+    border: solid red 1px;
+    padding: 5px;
+}
+
+main {
+    border: solid white 1px;
+    padding: 5px;
+}
+
+footer {
+    border: solid orange 1px;
+    padding: 5px;
+
+}
+*/
+
+.bg-custom {
+    background-color: #1e2363;
+}
+
+
+.footer-app {
+    display: flex;
+    align-items: center;
+    justify-content: space-evenly;
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    width: 100%;
+    background-color: #1e2363;
+    color: #c3c0d6;
+    padding: 10px;
+}
+
+.footer-btn {
+    background-color: #1e2363; /* Same as other buttons */
+    color: #c3c0d6;
+    padding: 15px 15px;
+    font-size: 16px;
+    text-align: center;
+    border-radius: 10px;
+    cursor: pointer;
+    border: none;
+    transition: background-color 0.3s ease;
+    text-decoration: none; /* Removes underline */
+    display: inline-block;
+}
+
+
+
+.header-app {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    background-color: #101a4d;
+    color: white;
+    padding: 10px;
+}
+
+/* *** body styles ***/
+body {
+    background-color: #323a9e;
+    font-family: "Times New Roman", Times, serif;
+}
+
+/* Stylesheet for the application */
+
+.logo {
+    display: block;
+    margin: 0 auto;
+    text-align: center;
+    max-width: 150px;
+}
+
+/* container styles */
+
+.page-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    text-align: center;
+    background-color: #323a9e;
+}
+
+/**** login styles *****/
+.generic-form{
+    max-width: 400px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+
+}
+.generic-form [type=text], .generic-form [type=password] {
+    padding: 10px;
+    font-size: 1.2rem;
+    border: #c3c0d6;
+    color: black;
+    border: none;
+}
+
+.generic-form button {
+    font-size: 1.2rem;
+    padding: 10px;
+    border-radius: 5px;
+    background-color: #535ce1;
+    color: white;
+    border: #c3c0d6;
+    border: none;
+}
+
+
+/**** styles to calculate time ****** */
+
+body {
+    font-family: "Times New Roman", Times, serif;
+}
+
+.calculate-container {
+    max-width: 400px;
+    margin: 100px;
+    padding: 20px;
+    border: 1px solid #ccc;
+    border-radius: 5px;
+}
+
+label {
+    display: block;
+    margin-bottom: 10px;
+}
+
+input[type="datetime-local"] {
+    width: 100%;
+    padding: 8px;
+    border: 1px solid #ccc;
+    border-radius: 4px;
+    box-sizing: border-box;
+    color: #1c1c1c;
+}
+
+button {
+    display: block;
+    width: 100%;
+    padding: 10px;
+    margin-top: 10px;
+    background-color: #c3c0d6;
+    color: #1e2363;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+}
+
+#result {
+    margin-top: 10px;
+    padding: 10px;
+    background-color: #f2f2f2;
+    border: 1px solid #c3c0d6;
+    border-radius: 4px;
+    color: #101a4d;
+}
+
+
+.text-silver {
+    color: #c3c0d6;
+}
+
+/*** style dashboard ***/
+.dashboard{
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    text-align: center;
+    max-width: 600px;
+    max-height: 600px;
+    margin-top: 100px;
+    margin-bottom: 200px;
+}
+
+/**** Dashboard graph styling *****/
+#sleepChart {
+    width: 100%;   /* Makes the chart width responsive */
+    height: 500px; /* Adjust height as per your need */
+}
diff --git a/src/main/resources/public/team12/images/SleepTrackerLogo.png b/src/main/resources/public/team12/images/SleepTrackerLogo.png
new file mode 100644
index 0000000..e1621b8
Binary files /dev/null and b/src/main/resources/public/team12/images/SleepTrackerLogo.png differ
diff --git a/src/main/resources/public/team12/images/color_palette.png b/src/main/resources/public/team12/images/color_palette.png
new file mode 100644
index 0000000..a74dd4e
Binary files /dev/null and b/src/main/resources/public/team12/images/color_palette.png differ
diff --git a/src/main/resources/public/team12/images/sleepGraphEks.png b/src/main/resources/public/team12/images/sleepGraphEks.png
new file mode 100644
index 0000000..51a0edb
Binary files /dev/null and b/src/main/resources/public/team12/images/sleepGraphEks.png differ
diff --git a/src/main/resources/public/team12/images/thumbnail.png b/src/main/resources/public/team12/images/thumbnail.png
new file mode 100644
index 0000000..76bdba0
Binary files /dev/null and b/src/main/resources/public/team12/images/thumbnail.png differ
diff --git a/src/main/resources/public/team12/sql/lifehackspring2025 b/src/main/resources/public/team12/sql/lifehackspring2025
new file mode 100644
index 0000000..112f3aa
Binary files /dev/null and b/src/main/resources/public/team12/sql/lifehackspring2025 differ
diff --git a/src/main/resources/public/team12/sql/lifehackspring2025DB.sql b/src/main/resources/public/team12/sql/lifehackspring2025DB.sql
new file mode 100644
index 0000000..7049e7d
--- /dev/null
+++ b/src/main/resources/public/team12/sql/lifehackspring2025DB.sql
@@ -0,0 +1,27 @@
+-- Users table
+CREATE TABLE team12_users (
+                              user_id SERIAL PRIMARY KEY,
+                              username VARCHAR(50) UNIQUE NOT NULL,
+                              password_hash VARCHAR(255) NOT NULL,
+                              created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Sleep Records table
+CREATE TABLE team12_sleep_records (
+                                      record_id SERIAL PRIMARY KEY,
+                                      user_id INT NOT NULL,
+                                      sleep_start TIMESTAMP NOT NULL,
+                                      sleep_end TIMESTAMP NOT NULL,
+                                      sleep_duration NUMERIC(5,2) GENERATED ALWAYS AS (EXTRACT(EPOCH FROM (sleep_end - sleep_start)) / 3600.0) STORED,
+                                      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                                      FOREIGN KEY (user_id) REFERENCES team12_users(user_id) ON DELETE CASCADE
+);
+
+-- Sleep Analytics Table
+CREATE TABLE team12_sleep_analytics (
+                                        analytics_id SERIAL PRIMARY KEY,
+                                        user_id INT NOT NULL,
+                                        total_sleep NUMERIC(5,2) NOT NULL,
+                                        last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+                                        FOREIGN KEY (user_id) REFERENCES team12_users(user_id) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/src/main/resources/public/team12/sql/sql_fill_DB.sql b/src/main/resources/public/team12/sql/sql_fill_DB.sql
new file mode 100644
index 0000000..d003069
--- /dev/null
+++ b/src/main/resources/public/team12/sql/sql_fill_DB.sql
@@ -0,0 +1,22 @@
+INSERT INTO team12_users (username, password_hash)
+VALUES
+    ('john_doe', 'hashed_password_123'),
+    ('jane_smith', 'hashed_password_456'),
+    ('alex_lee', 'hashed_password_789');
+
+
+INSERT INTO team12_sleep_records (user_id, sleep_start, sleep_end)
+VALUES
+    (1, '2025-03-18 22:00:00', '2025-03-19 06:00:00'),
+    (2, '2025-03-18 23:00:00', '2025-03-19 07:00:00'),
+    (3, '2025-03-18 21:30:00', '2025-03-19 05:30:00'),
+    (1, '2025-03-19 22:00:00', '2025-03-20 06:30:00'),
+    (2, '2025-03-19 23:30:00', '2025-03-20 07:30:00');
+
+
+
+INSERT INTO team12_sleep_analytics (user_id, total_sleep)
+VALUES
+    (1, 16.5),  -- 8 hours + 8.5 hours
+    (2, 16),    -- 8 hours + 8 hours
+    (3, 16);    -- 8 hours + 8 hours
diff --git a/src/main/resources/templates/team12/team12_createuser.html b/src/main/resources/templates/team12/team12_createuser.html
new file mode 100644
index 0000000..295a614
--- /dev/null
+++ b/src/main/resources/templates/team12/team12_createuser.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
+<title>FourThingsPlus</title>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link href="../../public/team12/css/team12_styles.css" th:href="@{/team12/css/team12_styles.css}" rel="stylesheet"/>
+</head>
+
+<body>
+<div class="page-container">
+    <header>
+        <a href="team12_index.html" th:href="@{/team12}">
+            <img class="logo" src="../../public/team12/images/SleepTrackerLogo.png"
+                 th:src="@{/team12/images/SleepTrackerLogo.png}" alt="Sleep Tracker Logo">        </a>
+    </header>
+
+
+    <h1>Here you can create a sleep profile</h1>
+
+    <form class="generic-form" method="post">
+        <input type="text" name="username" placeholder="Enter username"/>
+        <input type="password" name="password1" placeholder=" Enter password"/>
+        <input type="password" name="password2" placeholder=" Enter password again"/>
+
+        <button type="submit" formaction="/team12/createuser">Create profile</button>
+
+        <div><p th:text="${message}"></p></div>
+    </form>
+
+    <div >
+        <a href="team12_index.html" th:href="@{/team12}" class="text-silver"> Back to login page </a>
+    </div>
+
+</div>
+</body>
+</html>
diff --git a/src/main/resources/templates/team12/team12_dashboard.html b/src/main/resources/templates/team12/team12_dashboard.html
new file mode 100644
index 0000000..049a8f3
--- /dev/null
+++ b/src/main/resources/templates/team12/team12_dashboard.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
+<head>
+    <title>Dashboard</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link href="../../public/team12/css/team12_styles.css" th:href="@{/team12/css/team12_styles.css}" rel="stylesheet"/>
+    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
+
+    <style>
+        /* Custom styles for the chart size */
+        #sleepChart {
+            width: 100%;   /* Makes the chart responsive */
+            height: 500px; /* Adjust height as needed */
+        }
+    </style>
+</head>
+
+<body>
+<form>
+    <div class="page-container">
+        <header>
+            <a href="team12_index.html" th:href="@{/team12}"> </a>
+            <img class="logo" src="../../public/team12/images/SleepTrackerLogo.png"
+                 th:src="@{/team12/images/SleepTrackerLogo.png}" alt="Sleep Tracker Logo">
+            <h1>Welcome to your dashboard, <span th:text="${session.currentUser.username}"></span></h1>
+        </header>
+
+        <main>
+            <div class="dashboard">
+                <!-- The Canvas Element where the chart will be rendered -->
+                <canvas id="sleepChart"></canvas>
+            </div>
+        </main>
+
+        <footer class="footer-app">
+            <a th:if="${session.currentUser != null}" href="team12_index.html" th:href="@{/team12/logout}" class="text-silver"> Log out </a>
+            <a th:if="${session.currentUser != null}" href="team12_tracker.html" th:href="@{/team12/tracker}" class="text-silver"> Track some sleep </a>
+        </footer>
+
+    </div>
+</form>
+
+<!-- JavaScript to handle data fetching and rendering the bar chart -->
+<script>
+    // Assuming the data is fetched from /team12/sleep-data
+    fetch('/team12/sleep-data')
+        .then(response => response.json())
+        .then(data => {
+            if (data.length > 0) {
+                const labels = data.map(record => new Date(record.sleepStart).toLocaleDateString());
+                const durations = data.map(record => record.sleepDuration);
+
+                const ctx = document.getElementById('sleepChart').getContext('2d');
+                const sleepChart = new Chart(ctx, {
+                    type: 'bar',
+                    data: {
+                        labels: labels,
+                        datasets: [{
+                            label: 'Sleep Duration (hours)',
+                            data: durations,
+                            backgroundColor: 'rgba(75, 192, 192, 0.2)',
+                            borderColor: 'rgba(75, 192, 192, 1)',
+                            borderWidth: 1
+                        }]
+                    },
+                    options: {
+                        responsive: true,
+                        scales: {
+                            x: {
+                                beginAtZero: true,
+                                ticks: {
+                                    color: 'white', // Customize X-axis label color
+                                    font: {
+                                        size: 14, // Font size for X-axis labels
+                                    }
+                                }
+                            },
+                            y: {
+                                beginAtZero: true,
+                                ticks: {
+                                    color: 'white', // Customize Y-axis label color
+                                    font: {
+                                        size: 14, // Font size for Y-axis labels
+                                    }
+                                }
+                            }
+                        },
+                        plugins: {
+                            legend: {
+                                labels: {
+                                    color: 'white', // Customize legend label color
+                                    font: {
+                                        size: 16, // Font size for the legend labels
+                                    }
+                                }
+                            }
+                        }
+                    }
+                });
+            } else {
+                console.log("No sleep data available.");
+            }
+        })
+        .catch(error => {
+            console.error('Error fetching sleep data:', error);
+        });
+</script>
+
+</body>
+</html>
diff --git a/src/main/resources/templates/team12/team12_index.html b/src/main/resources/templates/team12/team12_index.html
new file mode 100644
index 0000000..2e1c480
--- /dev/null
+++ b/src/main/resources/templates/team12/team12_index.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
+<title>index</title>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link href="../../public/team12/css/team12_styles.css" th:href="@{/team12/css/team12_styles.css}" rel="stylesheet"/>
+</head>
+
+<body>
+<div class="page-container">
+    <header>
+        <h1>Welcome to Sleep Tracker</h1>
+        <a href="team12_index.html" th:href="@{/team12}">
+            <img class="logo" src="../../public/team12/images/SleepTrackerLogo.png"
+                 th:src="@{/team12/images/SleepTrackerLogo.png}" alt="Sleep Tracker Logo">
+        </a>
+        <br>
+    </header>
+
+    <main>
+
+        <form class="generic-form" method="post">
+            <input type="text" name="username" placeholder="Enter username"/>
+            <input type="password" name="password" placeholder="Enter password"/>
+
+            <button type="submit" formaction="/team12/login">Start tracking your sleep</button>
+
+            <div><p class="error" th:unless="${#strings.isEmpty(message)}" th:text="${message}"></p></div>
+        </form>
+    </main>
+
+    <footer class>
+        <div>
+            <a th:if="${session.currentUser != null}" href="team12_index.html" th:href="@{/team12/logout}" class="text-silver"> Log out </a>
+            <span th:if="${session.currentUser != null}"> |  </span>
+            <a href="team12_createuser.html" th:href="@{/team12/createuser}" class="text-silver">Create user </a>
+        </div>
+    </footer>
+
+</div>
+</body>
+</html>
diff --git a/src/main/resources/templates/team12/team12_tracker.html b/src/main/resources/templates/team12/team12_tracker.html
new file mode 100644
index 0000000..d64d2f0
--- /dev/null
+++ b/src/main/resources/templates/team12/team12_tracker.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
+<head>
+    <title>Sleeptracker</title>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link href="../../public/team12/css/team12_styles.css" th:href="@{/team12/css/team12_styles.css}" rel="stylesheet"/>
+</head>
+<div class="page-container">
+
+    <form method="post">
+        <div class="calculate-container">
+            <h2>Calculate & Register your sleep</h2>
+
+            <label for="sleep_start" class="text-silver">Going to sleep at:</label>
+            <input type="datetime-local" name="sleep_start" id="sleep_start" step="60" required>
+            <p></p>
+            <label for="sleep_end" class="text-silver">Waking up at:</label>
+            <input type="datetime-local" name="sleep_end" id="sleep_end" step="60" required>
+
+            <button type="reset">Clear</button>
+
+            <button type="button" id="calculate-btn">Calculate Duration</button>
+
+            <div id="result"></div>
+
+            <button type="submit" formaction="/team12/calculate">Register your sleep</button>
+
+
+        </div>
+    </form>
+
+    <script>
+        function formatDateTime(dateTime) {
+            const date = dateTime.toISOString().split('T')[0]; // Get date in YYYY-MM-DD format
+            const time = dateTime.toTimeString().split(' ')[0]; // Get time in HH:MM:SS format
+            return `${date} ${time}`;
+        }
+
+        function calculateDuration() {
+            const startTime = document.getElementById('sleep_start').value;
+            const endTime = document.getElementById('sleep_end').value;
+
+            if (!startTime || !endTime) {
+                document.getElementById('result').innerHTML = "<p style='color:red;'>Please enter both times.</p>";
+                return;
+            }
+
+            const startDate = new Date(startTime);
+            const endDate = new Date(endTime);
+
+            if (endDate <= startDate) {
+                document.getElementById('result').innerHTML = "<p style='color:red;'>End time must be after start time.</p>";
+                return;
+            }
+
+            const durationMs = endDate - startDate;
+            const hours = Math.floor(durationMs / (1000 * 60 * 60)); // Calculate hours
+            const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); // Calculate remaining minutes
+
+            const result = document.getElementById('result');
+            result.innerHTML = `
+        <p style='color:black;'>Sleeping at: <strong>${formatDateTime(startDate)}</strong></p>
+        <p style='color:black;'>Waking up at:<strong>${formatDateTime(endDate)}</strong></p>
+        <p style='color:black;'>Results in ${hours} hours, ${minutes} minutes of sleep</p>
+    `;
+        }
+
+        // Event listener for the Calculate button
+        document.getElementById('calculate-btn').addEventListener('click', function(e) {
+            e.preventDefault(); // Prevent form submission (no page reload)
+            calculateDuration();
+        });
+
+
+    </script>
+
+
+    <footer class="footer-app">
+        <a th:if="${session.currentUser != null}" href="team12_index.html" th:href="@{/team12/logout}" class="text-silver"> Log out </a>
+        <a th:if="${session.currentUser != null}" href="team12_dashboard.html" th:href="@{/team12/dashboard}" class="text-silver"> Dashboard </a>
+    </footer>
+</div>
+</body>
+</html>


Reviews:
user: login: jonbertelsen
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-22T00:45:35Z
user: login: jonbertelsen
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-22T00:47:06Z
user: login: jonbertelsen
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-22T00:47:57Z
user: login: jonbertelsen
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-22T00:48:26Z
user: login: jonbertelsen
body: Det ser godt ud. Jeg har skrevet kommentarer under "files changed". 

Inden jeg merger det ind, må I dertil meget gerne:

[ ] Tilføj et thumbnail billede på 200 x 200 pixels, som skal bruge til vores portalside - og som kan repræsentere jeres Lifehack applikation. Så lig den i images folderen under navnet: "thumbnail.png". Dvs, I skal gemme den i png format. Men super godt arbejde.
[ ] Commit og push jeres ændringer. I behøver ikke at lave et nyt PR, men kan evt. gå ind her på Github og skrive en kommentar i jeres PR som I allerede har oprettet.
Skriv endelig hvis I har spørgsmål.

Angående på mandag, så kan I evt. bare vise jeres local host udgave frem til demo. Det er jo ikke sikkert alle når at få fulgt op på mine kommentarer. 

Hilsen Jon
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-22T00:49:49Z
user: login: jonbertelsen
body: Super
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-22T22:00:10Z
user: login: jonbertelsen
body: Super
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-22T22:02:27Z

Review comments:
diff_hunk: @@ -0,0 +1,113 @@
+package app.controllers;
+
+import app.entitites.Team12SleepRecords;
+import app.entitites.Team12User;
+import app.exceptions.Team12DatabaseException;
+import app.persistence.ConnectionPool;
+import app.persistence.Team12SleepMapper;
+import app.persistence.Team12UserMapper;
+import io.javalin.Javalin;
+import io.javalin.http.Context;
+
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+
+public class Team12Controller {
+
+    public static void addRoutes(Javalin app, ConnectionPool connectionPool) {
+        app.get("/", ctx -> ctx.render("/team12/team12_index.html"));
path: src/main/java/app/controllers/Team12Controller.java
user: login: jonbertelsen
body: I må ikke tage "/".   Den skal jo bruges til den fælles forside. Så kald i stedet jeres route for "/team12" eller noget i den stil. 
created_at: 2025-03-22T00:45:35Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,113 @@
+package app.controllers;
+
+import app.entitites.Team12SleepRecords;
+import app.entitites.Team12User;
+import app.exceptions.Team12DatabaseException;
+import app.persistence.ConnectionPool;
+import app.persistence.Team12SleepMapper;
+import app.persistence.Team12UserMapper;
+import io.javalin.Javalin;
+import io.javalin.http.Context;
+
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+
+public class Team12Controller {
+
+    public static void addRoutes(Javalin app, ConnectionPool connectionPool) {
+        app.get("/", ctx -> ctx.render("/team12/team12_index.html"));
+        app.post("/login", ctx -> login(ctx, connectionPool));
path: src/main/java/app/controllers/Team12Controller.java
user: login: jonbertelsen
body: Det er også lidt farligt ikke at sætte "/team12" foran jeres routes. Det kunne jo sagtens være at andre grupper har samme routes - og så clasher de. Så sæt lige "/team12" foran og husk at ændre det i alle jeres links og forms. Lav evt. nogle søgninger på hver route og se hvor den bliver brugt.
created_at: 2025-03-22T00:47:06Z
author_association: OWNER
diff_hunk: @@ -19,22 +19,21 @@ public class Main {
 
     private static final ConnectionPool connectionPool = ConnectionPool.getInstance(USER, PASSWORD, URL, DB);
 
-    public static void main(String[] args)
-    {
+
+    public static void main(String[] args) {
 
 
         // Initializing Javalin and Jetty webserver
 
         Javalin app = Javalin.create(config -> {
             config.staticFiles.add("/public");
-            config.jetty.modifyServletContextHandler(handler ->  handler.setSessionHandler(SessionConfig.sessionConfig()));
+            config.jetty.modifyServletContextHandler(handler -> handler.setSessionHandler(SessionConfig.sessionConfig()));
             config.fileRenderer(new JavalinThymeleaf(ThymeleafConfig.templateEngine()));
         }).start(7070);
 
         // Routing
-
-        app.get("/", ctx ->  ctx.render("index.html"));
-        TeamTeacherController.routes(app);
+        Team12Controller.addRoutes(app, connectionPool);
+        //TeamTeacherController.routes(app);
path: src/main/java/app/Main.java
user: login: jonbertelsen
body: Ikke fjerne Signes og mine classer! Men jeg skal nok selv sætte dem ind.
created_at: 2025-03-22T00:47:57Z
author_association: OWNER
diff_hunk: @@ -19,22 +19,21 @@ public class Main {
 
     private static final ConnectionPool connectionPool = ConnectionPool.getInstance(USER, PASSWORD, URL, DB);
 
-    public static void main(String[] args)
-    {
+
+    public static void main(String[] args) {
 
 
         // Initializing Javalin and Jetty webserver
 
         Javalin app = Javalin.create(config -> {
             config.staticFiles.add("/public");
-            config.jetty.modifyServletContextHandler(handler ->  handler.setSessionHandler(SessionConfig.sessionConfig()));
+            config.jetty.modifyServletContextHandler(handler -> handler.setSessionHandler(SessionConfig.sessionConfig()));
             config.fileRenderer(new JavalinThymeleaf(ThymeleafConfig.templateEngine()));
         }).start(7070);
 
         // Routing
-
-        app.get("/", ctx ->  ctx.render("index.html"));
path: src/main/java/app/Main.java
user: login: jonbertelsen
body: Kald denne for "/team12" i stedet. Ellers stjæler I jo root route for den fælles forside.
created_at: 2025-03-22T00:48:26Z
author_association: OWNER

PR comments:
user: login: Mikkel-ao
created_at: 2025-03-22T12:20:01Z
body: Hvordan skal man håndtere root route ved opstart?
Synes ikke at jeg kan få javalin til at springe direkte til /team12 og er nødsaget til at have en "/" root route til egen html.

app.get("/", ctx -> ctx.render("/team12/team12_index.html"));
app.get("/team12", ctx -> ctx.render("/team12/team12_index.html"));
author_association: COLLABORATOR
user: login: jonbertelsen
created_at: 2025-03-22T12:50:40Z
body: Du skriver bare localhost:7070/team12 etc i hånden. Husk på at det hele skal samles
author_association: OWNER