From c697694089fb894fcb375ad88d3d67008a795567 Mon Sep 17 00:00:00 2001
From: Hannes <hannes-dev@mailbox.org>
Date: Sat, 20 Jul 2024 15:30:26 +0200
Subject: [PATCH] zess: add a frontend

* vinvoor: init

* zess: development docker

* vingo: customizable redirect

* zess: add -c option

* vinvoor: add login

* vingo: add cors

* vingo: add json serialization

* vinvoor: add login and logout

* vinvoor: add a cards page

* zess: add development instructions

* vinvoor: add a theme

* vinvoor: refactor

* vinvoor: add support for dark and light mode

* vinvoor: move url's to .env

* zess: start vinvoor as non root user

* vinvoor: add a cards overview

* vingo: more serialization

* vinvoor: refactor the cards page

* vinvoor: change to camelCase

* vinvoor: add a leaderboard

* vinvoor: add a welcome page

* zess: change env var to export in dev script

* vingo: add database migrations

* vingo: fix api returning null when no results

* vingo: add card register via api

* vingo: add card id and name

* vinvoor: add a github activity style overview

* vinvoor: support adding new cards

* vinvoor: show current checkin status

* vinvoor: show current streak days

* vinvoor: show the user's most common days

* vinvoor: simple overview page

* vinvoor: fix crash when no scans are registered

* vinvoor: center graphs in the overview

* vinvoor: add support for new comers

* vingo: move to gorm

* vingo: go get -u && go mod tidy

* vingo: card register status endpoint

* vingo: ability to add name to card

* vingo: return if last register was success

* vingo: return extra stats for cards

* vingo: time remaining on card status endpoint

* vinvoor: support new card features

* vingo: add leaderboard position change

* vingo: pieter post pieter post pieter post verdient de kost (met zijn post)

* vinvoor: pieter post deed ambetant

* vinvoor: show position changes in the leaderboard

* vinvoor: show a scan overview

* vingo: fix scans

* vingo: card handlers to receivers

* vingo: settings endpoint

* vingo: cleanup

* vinvoor: add a new zess logo

---------

Co-authored-by: Topvennie <vincent@vallaeys.com>
---
 vingo/database/cards.go                       |   48 +-
 vingo/database/days.go                        |    9 -
 vingo/database/db.go                          |   41 +-
 vingo/database/models.go                      |   70 +
 vingo/database/scans.go                       |   65 +-
 vingo/database/settings.go                    |   24 +-
 vingo/database/users.go                       |   57 +-
 vingo/go.mod                                  |   26 +-
 vingo/go.sum                                  |   50 +-
 vingo/handlers/access.go                      |    8 -
 vingo/handlers/cards.go                       |   52 +-
 vingo/handlers/leaderboard.go                 |   18 +-
 vingo/handlers/pages.go                       |   88 --
 vingo/handlers/scans.go                       |    1 +
 vingo/handlers/settings.go                    |   33 +-
 vingo/handlers/store.go                       |    7 +-
 vingo/layouts/cards.html                      |   39 -
 vingo/layouts/days.html                       |   39 -
 vingo/layouts/landing.html                    |    1 -
 vingo/layouts/leaderboard.html                |   18 -
 vingo/layouts/main.html                       |  171 ---
 vingo/layouts/partials/navbar.html            |   43 -
 vingo/layouts/scans.html                      |   20 -
 vingo/layouts/settings.html                   |   29 -
 vingo/layouts/stats.html                      |   30 -
 vingo/main.go                                 |   51 +-
 vinvoor/index.html                            |    2 +-
 vinvoor/package.json                          |   11 +-
 vinvoor/public/logo.svg                       |    1 +
 vinvoor/src/App.tsx                           |   13 +-
 vinvoor/src/WelcomePage.tsx                   |   19 +-
 vinvoor/src/cards/Cards.tsx                   |   28 +-
 vinvoor/src/cards/CardsAdd.tsx                |  180 ++-
 vinvoor/src/cards/CardsDelete.tsx             |   72 +-
 vinvoor/src/cards/CardsTable.tsx              |   53 +-
 vinvoor/src/cards/CardsTableBody.tsx          |   89 +-
 vinvoor/src/cards/CardsTableHead.tsx          |    6 +-
 vinvoor/src/cards/CardsTableToolbar.tsx       |   11 +-
 vinvoor/src/cards/CircularTimeProgress.tsx    |   31 +
 vinvoor/src/components/ConfirmationModal.tsx  |   61 -
 vinvoor/src/components/UnstyledLink.tsx       |    4 +-
 vinvoor/src/hooks/useFetch.ts                 |    7 +-
 vinvoor/src/leaderboard/Leaderboard.tsx       |    3 +-
 .../src/leaderboard/LeaderboardTableBody.tsx  |  121 +-
 vinvoor/src/main.tsx                          |   32 +-
 vinvoor/src/navbar/NavBar.tsx                 |   33 +-
 vinvoor/src/navbar/NavBarLogo.tsx             |   41 +-
 vinvoor/src/navbar/NavBarPages.tsx            |   10 +-
 vinvoor/src/navbar/NavBarSandwich.tsx         |    8 +-
 vinvoor/src/navbar/NavBarUserMenu.tsx         |   44 +-
 vinvoor/src/overview/Overview.tsx             |  127 ++
 vinvoor/src/overview/checkin/CheckIn.tsx      |   37 +
 vinvoor/src/overview/days/Days.tsx            |   58 +
 vinvoor/src/overview/heatmap/Heatmap.tsx      |  257 ++++
 vinvoor/src/overview/heatmap/heatmap.css      |   76 ++
 vinvoor/src/overview/heatmap/utils.ts         |  101 ++
 vinvoor/src/overview/streak/Streak.tsx        |   94 ++
 vinvoor/src/scans/Scans.tsx                   |   36 +
 vinvoor/src/scans/ScansBody.tsx               |   43 +
 .../ScansTableHead.tsx}                       |    6 +-
 vinvoor/src/settings/Settings.tsx             |    0
 vinvoor/src/types/cards.ts                    |   66 +-
 vinvoor/src/types/{table.ts => general.ts}    |   11 +
 vinvoor/src/types/leaderboard.ts              |   10 +-
 vinvoor/src/types/scans.ts                    |   52 +
 vinvoor/src/user/Login.tsx                    |   17 +-
 vinvoor/src/user/Logout.tsx                   |   19 +-
 vinvoor/src/user/UserProvider.tsx             |    4 +-
 vinvoor/src/util/fetch.ts                     |   58 +-
 vinvoor/src/util/util.ts                      |   60 +
 vinvoor/vite.config.ts                        |    3 +-
 vinvoor/yarn.lock                             | 1144 ++++++++++++-----
 72 files changed, 2750 insertions(+), 1447 deletions(-)
 create mode 100644 vingo/database/models.go
 delete mode 100644 vingo/handlers/pages.go
 delete mode 100644 vingo/layouts/cards.html
 delete mode 100644 vingo/layouts/days.html
 delete mode 100644 vingo/layouts/landing.html
 delete mode 100644 vingo/layouts/leaderboard.html
 delete mode 100644 vingo/layouts/main.html
 delete mode 100644 vingo/layouts/partials/navbar.html
 delete mode 100644 vingo/layouts/scans.html
 delete mode 100644 vingo/layouts/settings.html
 delete mode 100644 vingo/layouts/stats.html
 create mode 100644 vinvoor/public/logo.svg
 create mode 100644 vinvoor/src/cards/CircularTimeProgress.tsx
 delete mode 100644 vinvoor/src/components/ConfirmationModal.tsx
 create mode 100644 vinvoor/src/overview/Overview.tsx
 create mode 100644 vinvoor/src/overview/checkin/CheckIn.tsx
 create mode 100644 vinvoor/src/overview/days/Days.tsx
 create mode 100644 vinvoor/src/overview/heatmap/Heatmap.tsx
 create mode 100644 vinvoor/src/overview/heatmap/heatmap.css
 create mode 100644 vinvoor/src/overview/heatmap/utils.ts
 create mode 100644 vinvoor/src/overview/streak/Streak.tsx
 create mode 100644 vinvoor/src/scans/Scans.tsx
 create mode 100644 vinvoor/src/scans/ScansBody.tsx
 rename vinvoor/src/{leaderboard/LeaderboardTableHead.tsx => scans/ScansTableHead.tsx} (69%)
 create mode 100644 vinvoor/src/settings/Settings.tsx
 rename vinvoor/src/types/{table.ts => general.ts} (63%)
 create mode 100644 vinvoor/src/types/scans.ts
 create mode 100644 vinvoor/src/util/util.ts

diff --git a/vingo/database/cards.go b/vingo/database/cards.go
index ca05186..15962b8 100644
--- a/vingo/database/cards.go
+++ b/vingo/database/cards.go
@@ -1,43 +1,35 @@
 package database
 
-import "time"
-
-type Card struct {
-	Serial    string    `json:"serial"`
-	CreatedAt time.Time `json:"createdAt"`
-}
-
-var (
-	cardsCreateStmt = `
-		CREATE TABLE IF NOT EXISTS cards (
-			serial TEXT NOT NULL PRIMARY KEY UNIQUE,
-			created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
-			user_id INTEGER NOT NULL REFERENCES users(id)
-		);
-	`
-)
-
 func CreateCard(serial string, user_id int) error {
-	_, err := db.Exec("INSERT INTO cards (serial, user_id) VALUES ($1, $2);", serial, user_id)
-	return err
+	return gorm_db.Create(&Card{Serial: serial, UserId: user_id}).Error
 }
 
 func GetCardsForUser(user_id int) ([]Card, error) {
-	rows, err := db.Query("SELECT serial, created_at FROM cards WHERE user_id = $1;", user_id)
+	var cards []Card
+	result := gorm_db.Where("user_id = ?", user_id).Find(&cards)
+	return cards, result.Error
+}
+
+func GetCardsAndStatsForUser(user_id int) ([]CardAPI, error) {
+	rows, err := db.Query(`
+	SELECT cards.id, cards.created_at, serial, name, COUNT(scans.id), (select MAX(scan_time) from scans where card_serial = cards.serial) from cards LEFT JOIN scans on scans.card_serial = serial WHERE
+	user_id = $1 GROUP BY cards.id;
+	`, user_id)
+
 	if err != nil {
 		return nil, err
 	}
-	defer rows.Close()
 
-	cards := make([]Card, 0)
+	cards := []CardAPI{}
 	for rows.Next() {
-		var card Card
-		err := rows.Scan(&card.Serial, &card.CreatedAt)
-		if err != nil {
-			return nil, err
-		}
-		cards = append(cards, card)
+		var item CardAPI
+		_ = rows.Scan(&item.Id, &item.CreatedAt, &item.Serial, &item.Name, &item.AmountUsed, &item.LastUsed)
+		cards = append(cards, item)
 	}
 
 	return cards, nil
 }
+
+func UpdateCardName(id int, name string, user_id int) error {
+	return gorm_db.Model(&Card{}).Where("id = ? AND user_id = ?", id, user_id).Update("name", name).Error
+}
diff --git a/vingo/database/days.go b/vingo/database/days.go
index f78688a..3187cb9 100644
--- a/vingo/database/days.go
+++ b/vingo/database/days.go
@@ -7,15 +7,6 @@ type Day struct {
 	Date time.Time
 }
 
-var (
-	daysCreateStmt = `
-		CREATE TABLE IF NOT EXISTS days (
-			id SERIAL NOT NULL PRIMARY KEY,
-			date DATE NOT NULL UNIQUE
-		);
-		`
-)
-
 func CreateDays(first_day time.Time, last_day time.Time) error {
 	tx, err := db.Begin()
 	if err != nil {
diff --git a/vingo/database/db.go b/vingo/database/db.go
index 63b6768..157c7ae 100644
--- a/vingo/database/db.go
+++ b/vingo/database/db.go
@@ -3,34 +3,39 @@ package database
 import (
 	"database/sql"
 	"log"
+
+	"gorm.io/driver/postgres"
+	"gorm.io/gorm"
 )
 
 var (
-	db *sql.DB
+	db      *sql.DB
+	gorm_db *gorm.DB
 )
 
-func createTables() {
-	// Tables to create
-	createStmts := []string{usersCreateStmt, settingsCreateStmt, cardsCreateStmt, scansCreateStmt, daysCreateStmt}
-	for _, stmt := range createStmts {
-		_, err := db.Exec(stmt)
-		if err != nil {
-			log.Println("Error creating table with query: \n", stmt)
-			log.Fatal(err)
-		}
-	}
-}
-
 func Get() *sql.DB {
 	return db
 }
 
-func OpenDatabase(conn string) {
-	new_db, err := sql.Open("postgres", conn)
+func OpenDatabase(db_string string) {
+	new_db, err := gorm.Open(postgres.Open(db_string), &gorm.Config{})
 	if err != nil {
-		log.Panicln("Error opening database connection")
+		log.Println("Error opening database connection")
 		log.Fatal(err)
 	}
-	db = new_db
-	createTables()
+
+	err = new_db.AutoMigrate()
+	if err != nil {
+		log.Println("Error migrating database")
+		log.Fatal(err)
+	}
+
+	err = new_db.AutoMigrate(&User{}, &Card{}, &Scan{}, &Day{}, &Settings{}, &Season{})
+	if err != nil {
+		log.Println("Error migrating database")
+		log.Fatal(err)
+	}
+
+	gorm_db = new_db
+	db, _ = new_db.DB()
 }
diff --git a/vingo/database/models.go b/vingo/database/models.go
new file mode 100644
index 0000000..ce66161
--- /dev/null
+++ b/vingo/database/models.go
@@ -0,0 +1,70 @@
+package database
+
+import (
+	"time"
+
+	"gorm.io/gorm"
+)
+
+type BaseModel struct {
+	Id        int            `json:"id" gorm:"primarykey"`
+	CreatedAt time.Time      `json:"createdAt"`
+	UpdatedAt time.Time      `json:"updatedAt"`
+	DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
+}
+
+type User struct {
+	BaseModel
+	Username   string `json:"username"`
+	Admin      bool   `json:"admin"`
+	SettingsId int
+	Settings   Settings `json:"settings"`
+	Cards      []Card   `json:"-" gorm:"foreignKey:UserId;references:Id"`
+}
+
+type Settings struct {
+	BaseModel
+	ScanInOut   bool `json:"scanInOut"`
+	Leaderboard bool `json:"leaderboard"`
+	Public      bool `json:"public"`
+}
+
+type Card struct {
+	BaseModel
+	Serial string `gorm:"uniqueIndex"`
+	Name   string
+	UserId int
+	User   User
+	Scans  []Scan `gorm:"foreignKey:CardSerial;references:Serial"`
+}
+
+func Card_to_API(card Card) CardAPI {
+	var lastUsed time.Time = card.CreatedAt
+	if len(card.Scans) != 0 {
+		lastUsed = card.Scans[len(card.Scans)-1].ScanTime
+	}
+
+	return CardAPI{
+		Id:         card.Id,
+		Serial:     card.Serial,
+		Name:       card.Name,
+		LastUsed:   lastUsed,
+		AmountUsed: len(card.Scans),
+	}
+}
+
+type CardAPI struct {
+	Id         int       `json:"id"`
+	CreatedAt  time.Time `json:"createdAt"`
+	Serial     string    `json:"serial"`
+	Name       string    `json:"name"`
+	LastUsed   time.Time `json:"lastUsed"`
+	AmountUsed int       `json:"amountUsed"`
+}
+
+type Scan struct {
+	BaseModel
+	ScanTime   time.Time `json:"scanTime"`
+	CardSerial string    `json:"cardSerial" gorm:"index"`
+	Card       Card      `json:"-" gorm:"foreignKey:CardSerial;references:Serial"`
+}
diff --git a/vingo/database/scans.go b/vingo/database/scans.go
index 3aa0aa2..36347f4 100644
--- a/vingo/database/scans.go
+++ b/vingo/database/scans.go
@@ -1,11 +1,8 @@
 package database
 
-import "time"
-
-type Scan struct {
-	ScanTime time.Time `json:"scanTime"`
-	Card     string    `json:"card"`
-}
+import (
+	"time"
+)
 
 type Present struct {
 	Date      time.Time
@@ -14,42 +11,27 @@ type Present struct {
 }
 
 type LeaderboardItem struct {
-	Position  int    `json:"position"`
-	Username  string `json:"username"`
-	TotalDays int    `json:"totalDays"`
+	Position       int    `json:"position"`
+	UserId         int    `json:"userId"`
+	Username       string `json:"username"`
+	TotalDays      int    `json:"totalDays"`
+	PositionChange int    `json:"positionChange"`
 }
 
-var (
-	scansCreateStmt = `
-		CREATE TABLE IF NOT EXISTS scans (
-			id SERIAL NOT NULL PRIMARY KEY,
-			scan_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
-			scan_in BOOLEAN,
-			card_serial TEXT NOT NULL REFERENCES cards(serial)
-		);
-	`
-)
-
 func CreateScan(card_serial string) error {
-	_, err := db.Exec("INSERT INTO scans (card_serial) VALUES ($1);", card_serial)
-	return err
+	return gorm_db.Create(&Scan{ScanTime: time.Now(), CardSerial: card_serial}).Error
 }
 
 func GetScansForUser(user_id int) ([]Scan, error) {
-	scans_rows, err := db.Query("SELECT scan_time, card_serial FROM scans WHERE card_serial IN (SELECT serial FROM cards WHERE user_id = $1) ORDER BY scan_time DESC;", user_id)
-	if err != nil {
-		return nil, err
-	}
+	var user User
+	result := gorm_db.Preload("Cards.Scans").First(&user, user_id)
 
 	var scans []Scan
-
-	for scans_rows.Next() {
-		var scan Scan
-		_ = scans_rows.Scan(&scan.ScanTime, &scan.Card)
-
-		scans = append(scans, scan)
+	for _, card := range user.Cards {
+		scans = append(scans, card.Scans...)
 	}
-	return scans, nil
+
+	return scans, result.Error
 }
 
 func GetPresenceHistory(user_id int) ([]Present, error) {
@@ -78,7 +60,7 @@ func GetPresenceHistory(user_id int) ([]Present, error) {
 		return nil, err
 	}
 
-	var presences []Present
+	presences := []Present{}
 	for rows.Next() {
 		var present Present
 		_ = rows.Scan(&present.Date, &present.Present, &present.StreakDay)
@@ -89,24 +71,25 @@ func GetPresenceHistory(user_id int) ([]Present, error) {
 	return presences, nil
 }
 
-func TotalDaysPerUser() ([]LeaderboardItem, error) {
+func TotalDaysPerUser(before_time time.Time) ([]LeaderboardItem, error) {
 	rows, err := db.Query(`
-	SELECT count, username, RANK() OVER (ORDER BY count desc) AS position
-	FROM (SELECT COUNT(DISTINCT ((scan_time - INTERVAL '4 hours') AT TIME ZONE 'Europe/Brussels')::date), username
+	SELECT user_id, count, username, RANK() OVER (ORDER BY count desc) AS position
+	FROM (SELECT COUNT(DISTINCT ((scan_time - INTERVAL '4 hours') AT TIME ZONE 'Europe/Brussels')::date), username, users.id as user_id
 		FROM scans
 			LEFT JOIN cards ON card_serial = serial
 			LEFT JOIN users ON user_id = users.id
-			GROUP BY username);
-	`)
+			WHERE scan_time < $1
+			GROUP BY username, users.id);
+	`, before_time)
 
 	if err != nil {
 		return nil, err
 	}
 
-	var leaderboard []LeaderboardItem
+	leaderboard := []LeaderboardItem{}
 	for rows.Next() {
 		var item LeaderboardItem
-		_ = rows.Scan(&item.TotalDays, &item.Username, &item.Position)
+		_ = rows.Scan(&item.UserId, &item.TotalDays, &item.Username, &item.Position)
 
 		leaderboard = append(leaderboard, item)
 	}
diff --git a/vingo/database/settings.go b/vingo/database/settings.go
index 8ccd784..c26f98b 100644
--- a/vingo/database/settings.go
+++ b/vingo/database/settings.go
@@ -1,32 +1,14 @@
 package database
 
-type Settings struct {
-	ScanInOut   bool `json:"scanInOut"`
-	Leaderboard bool `json:"leaderboard"`
-	Public      bool `json:"public"`
-}
-
-var (
-	settingsCreateStmt = `
-		CREATE TABLE IF NOT EXISTS settings (
-			user_id INT NOT NULL PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
-			public BOOLEAN NOT NULL DEFAULT FALSE,
-			scan_in_out BOOLEAN NOT NULL DEFAULT FALSE,
-			leaderboard BOOLEAN NOT NULL DEFAULT TRUE
-		);
-	`
-)
-
 func CreateSettings(user_id int) error {
 	_, err := db.Exec("INSERT INTO settings (user_id) VALUES ($1) ON CONFLICT DO NOTHING;", user_id)
 	return err
 }
 
 func GetSettings(user_id int) (*Settings, error) {
-	row := db.QueryRow("SELECT scan_in_out, leaderboard, public FROM settings WHERE user_id = $1;", user_id)
-	settings := new(Settings)
-	err := row.Scan(&settings.ScanInOut, &settings.Leaderboard, &settings.Public)
-	return settings, err
+	var settings Settings
+	result := gorm_db.First(&settings, "user_id = ?", user_id)
+	return &settings, result.Error
 }
 
 func UpdateSettings(user_id int, settings Settings) error {
diff --git a/vingo/database/users.go b/vingo/database/users.go
index e46a148..123d93d 100644
--- a/vingo/database/users.go
+++ b/vingo/database/users.go
@@ -1,55 +1,26 @@
 package database
 
-type User struct {
-	Id       int      `json:"id"`
-	Username string   `json:"username"`
-	Admin    bool     `json:"admin"`
-	Settings Settings `json:"settings"`
-}
-
-var (
-	usersCreateStmt = `
-		CREATE TABLE IF NOT EXISTS users (
-			id INTEGER PRIMARY KEY,
-			username TEXT NOT NULL,
-			admin BOOLEAN DEFAULT FALSE NOT NULL,
-			created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
-		);
-	`
+import (
+	"time"
 )
 
 func CreateUserIfNew(user_id int, username string) error {
-	_, err := db.Exec("INSERT INTO users (id, username) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET username = EXCLUDED.username;", user_id, username)
-	if err != nil {
-		return err
-	}
-
-	err = CreateSettings(user_id)
-	return err
+	var user = &User{Username: username, Settings: Settings{ScanInOut: false, Leaderboard: true, Public: false}}
+	user.Id = user_id
+	user.Settings.CreatedAt = time.Now()
+	user.Settings.UpdatedAt = time.Now()
+	result := gorm_db.FirstOrCreate(&user)
+	return result.Error
 }
 
 func GetUser(user_id int) (*User, error) {
-	return getUser("SELECT id, username, admin, scan_in_out, leaderboard, public FROM users JOIN settings on id = user_id WHERE id = $1;", user_id)
+	var user User
+	result := gorm_db.First(&user, user_id)
+	return &user, result.Error
 }
 
 func GetUserFromCard(card_serial string) (*User, error) {
-	row := db.QueryRow(`
-		SELECT users.id, users.username, users.admin
-		FROM users
-		JOIN cards ON users.id = cards.user_id
-		WHERE cards.serial = $1;
-	`, card_serial)
-	user := new(User)
-	err := row.Scan(&user.Id, &user.Username, &user.Admin)
-	return user, err
-}
-
-func getUser(query string, args ...interface{}) (*User, error) {
-	row := db.QueryRow(query, args...)
-	user := new(User)
-	settings := new(Settings)
-	err := row.Scan(&user.Id, &user.Username, &user.Admin, &settings.ScanInOut, &settings.Leaderboard, &settings.Public)
-
-	user.Settings = *settings
-	return user, err
+	var card Card
+	result := gorm_db.First(&card, "serial = ?", card_serial)
+	return &card.User, result.Error
 }
diff --git a/vingo/go.mod b/vingo/go.mod
index fecd3f4..5c1a039 100644
--- a/vingo/go.mod
+++ b/vingo/go.mod
@@ -3,24 +3,36 @@ module vingo
 go 1.22.1
 
 require (
-	github.com/gofiber/fiber/v2 v2.52.4
-	github.com/gofiber/template/html/v2 v2.1.1
+	github.com/gofiber/fiber/v2 v2.52.5
 	github.com/joho/godotenv v1.5.1
 	github.com/lib/pq v1.10.9
+	gorm.io/driver/postgres v1.5.9
+	gorm.io/gorm v1.25.11
+)
+
+require (
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+	github.com/jackc/pgx/v5 v5.6.0 // indirect
+	github.com/jackc/puddle/v2 v2.2.1 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	golang.org/x/crypto v0.25.0 // indirect
+	golang.org/x/sync v0.7.0 // indirect
+	golang.org/x/text v0.16.0 // indirect
 )
 
 require (
 	github.com/andybalholm/brotli v1.1.0 // indirect
-	github.com/gofiber/template v1.8.3 // indirect
-	github.com/gofiber/utils v1.1.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
-	github.com/klauspost/compress v1.17.8 // indirect
+	github.com/klauspost/compress v1.17.9 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.15 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
+	github.com/stretchr/testify v1.8.4 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	github.com/valyala/fasthttp v1.53.0 // indirect
+	github.com/valyala/fasthttp v1.55.0 // indirect
 	github.com/valyala/tcplisten v1.0.0 // indirect
-	golang.org/x/sys v0.20.0 // indirect
+	golang.org/x/sys v0.22.0 // indirect
 )
diff --git a/vingo/go.sum b/vingo/go.sum
index 46246cd..5fce104 100644
--- a/vingo/go.sum
+++ b/vingo/go.sum
@@ -1,21 +1,28 @@
 github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
-github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
-github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
-github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
-github.com/gofiber/template/html/v2 v2.1.1 h1:QEy3O3EBkvwDthy5bXVGUseOyO6ldJoiDxlF4+MJiV8=
-github.com/gofiber/template/html/v2 v2.1.1/go.mod h1:2G0GHHOUx70C1LDncoBpe4T6maQbNa4x1CVNFW0wju0=
-github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
-github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
+github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
+github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
+github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
-github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -30,17 +37,32 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.53.0 h1:lW/+SUkOxCx2vlIu0iaImv4JLrVRnbbkpCoaawvA4zc=
-github.com/valyala/fasthttp v1.53.0/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM=
+github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
+github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
 github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
 github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
+golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
-golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
+gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
+gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
+gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
diff --git a/vingo/handlers/access.go b/vingo/handlers/access.go
index cf21b1f..e1ed9d0 100644
--- a/vingo/handlers/access.go
+++ b/vingo/handlers/access.go
@@ -3,14 +3,6 @@ package handlers
 import "github.com/gofiber/fiber/v2"
 
 func IsLoggedIn(c *fiber.Ctx) error {
-	if getUserFromStore(c) == nil {
-		return c.Redirect("/login")
-	}
-
-	return c.Next()
-}
-
-func IsLoggedInAPI(c *fiber.Ctx) error {
 	if getUserFromStore(c) == nil {
 		return c.Status(401).SendString("Unauthorized")
 	}
diff --git a/vingo/handlers/cards.go b/vingo/handlers/cards.go
index 2dd06a3..accd969 100644
--- a/vingo/handlers/cards.go
+++ b/vingo/handlers/cards.go
@@ -1,32 +1,37 @@
 package handlers
 
 import (
+	"strconv"
 	"time"
 	"vingo/database"
 
 	"github.com/gofiber/fiber/v2"
 )
 
-func StartCardRegister(c *fiber.Ctx) error {
-	// keep track of the user that initiated the request in global state
-	// since only one user can be registering a card at a time
+var register_timeout = time.Minute
+
+type Cards struct{}
+
+func (Cards) StartRegister(c *fiber.Ctx) error {
 	user := getUserFromStore(c)
 
 	if time.Now().Before(registering_end) {
-		return c.Status(400).SendString("Another user is already registering a card")
+		// true if current user is already registering
+		return c.Status(503).JSON(map[string]bool{"isCurrentUser": registering_user == user.Id})
 	}
 
 	registering_user = user.Id
-	registering_end = time.Now().Add(time.Minute)
+	registering_end = time.Now().Add(register_timeout)
+	registering_success = false
 
 	logger.Println("Card registration started by user", registering_user)
 
-	return c.Status(200).Redirect("/cards")
+	return c.Status(200).JSON(map[string]bool{})
 }
 
-func Cards(c *fiber.Ctx) error {
+func (Cards) Get(c *fiber.Ctx) error {
 	user := getUserFromStore(c)
-	cards, err := database.GetCardsForUser(user.Id)
+	cards, err := database.GetCardsAndStatsForUser(user.Id)
 	if err != nil {
 		logger.Println("", err)
 		return c.Status(500).SendString("Error getting cards")
@@ -34,3 +39,34 @@ func Cards(c *fiber.Ctx) error {
 
 	return c.JSON(cards)
 }
+
+func (Cards) RegisterStatus(c *fiber.Ctx) error {
+	user := getUserFromStore(c)
+	register_ongoing := time.Now().Before(registering_end)
+	is_current_user := registering_user == user.Id
+	time_remaining := time.Until(registering_end).Seconds()
+	time_percentage := time_remaining / register_timeout.Seconds()
+	return c.JSON(map[string]interface{}{"registering": register_ongoing, "isCurrentUser": is_current_user, "success": registering_success, "timeRemaining": time_remaining, "timePercentage": time_percentage})
+}
+
+func (Cards) Update(c *fiber.Ctx) error {
+	user := getUserFromStore(c)
+	card_id, err := strconv.Atoi(c.Params("id"))
+	if err != nil {
+		logger.Println(err)
+		return c.Status(400).SendString("Invalid card id")
+	}
+
+	payload := struct {
+		Name string `json:"name"`
+	}{}
+	c.BodyParser(&payload)
+
+	err = database.UpdateCardName(card_id, payload.Name, user.Id)
+	if err != nil {
+		logger.Println(err)
+		return c.Status(500).SendString("Error updating card name")
+	}
+
+	return c.Status(200).JSON(map[string]bool{})
+}
diff --git a/vingo/handlers/leaderboard.go b/vingo/handlers/leaderboard.go
index f99f672..486dd04 100644
--- a/vingo/handlers/leaderboard.go
+++ b/vingo/handlers/leaderboard.go
@@ -1,17 +1,33 @@
 package handlers
 
 import (
+	"time"
 	"vingo/database"
 
 	"github.com/gofiber/fiber/v2"
 )
 
 func Leaderboard(c *fiber.Ctx) error {
-	users, err := database.TotalDaysPerUser()
+	users, err := database.TotalDaysPerUser(time.Now())
 	if err != nil {
 		logger.Println("Error getting leaderboard:", err)
 		return c.Status(500).SendString("Error getting leaderboard")
 	}
 
+	users_last_week, err := database.TotalDaysPerUser(time.Now().AddDate(0, 0, -7))
+	if err != nil {
+		logger.Println("Error getting leaderboard:", err)
+		return c.Status(500).SendString("Error getting leaderboard")
+	}
+
+	for i, user := range users {
+		for _, user_last_week := range users_last_week {
+			if user.UserId == user_last_week.UserId {
+				users[i].PositionChange = user_last_week.Position - user.Position
+				break
+			}
+		}
+	}
+
 	return c.JSON(users)
 }
diff --git a/vingo/handlers/pages.go b/vingo/handlers/pages.go
deleted file mode 100644
index 56067f2..0000000
--- a/vingo/handlers/pages.go
+++ /dev/null
@@ -1,88 +0,0 @@
-package handlers
-
-import (
-	"time"
-	"vingo/database"
-
-	"github.com/gofiber/fiber/v2"
-)
-
-func Index(c *fiber.Ctx) error {
-	current_user := getUserFromStore(c)
-	if current_user != nil {
-		return stats(c, current_user)
-	} else {
-		return landing(c)
-	}
-}
-
-func landing(c *fiber.Ctx) error {
-	return c.Render("landing", nil, "main")
-}
-
-func stats(c *fiber.Ctx, user *database.User) error {
-	days, err := database.GetPresenceHistory(user.Id)
-	if err != nil {
-		logger.Println("Error get presence history:", err)
-		return c.Status(500).SendString("Error getting presence history")
-	}
-
-	return c.Render("stats", fiber.Map{"user": user, "days_present_7": days}, "main")
-}
-
-func ScansPage(c *fiber.Ctx) error {
-	current_user := getUserFromStore(c)
-
-	scans, err := database.GetScansForUser(current_user.Id)
-	if err != nil {
-		logger.Println("Error get scans:", err)
-		return c.Status(500).SendString("Error getting scans")
-	}
-
-	return c.Render("scans", fiber.Map{"user": current_user, "scans": scans}, "main")
-}
-
-func CardsPage(c *fiber.Ctx) error {
-	current_user := getUserFromStore(c)
-
-	cards, err := database.GetCardsForUser(current_user.Id)
-	if err != nil {
-		logger.Println("", err)
-		return c.Status(500).SendString("Error getting cards")
-	}
-
-	registering := time.Now().Before(registering_end)
-	registering_is_user := current_user.Id == registering_user
-
-	return c.Render("cards", fiber.Map{"user": current_user, "cards": cards, "registering": registering, "reg_user": registering_is_user}, "main")
-}
-
-func DaysPage(c *fiber.Ctx) error {
-	current_user := getUserFromStore(c)
-
-	days, err := database.GetDays()
-	if err != nil {
-		logger.Println("Error get days:", err)
-		return c.Status(500).SendString("Error getting days")
-	}
-
-	return c.Render("days", fiber.Map{"user": current_user, "days": days}, "main")
-}
-
-func LeaderboardPage(c *fiber.Ctx) error {
-	current_user := getUserFromStore(c)
-
-	leaderboard, err := database.TotalDaysPerUser()
-	if err != nil {
-		logger.Println("Error getting leaderboard:", err)
-		return c.Status(500).SendString("Error getting leaderboard")
-	}
-
-	return c.Render("leaderboard", fiber.Map{"user": current_user, "leaderboard": leaderboard}, "main")
-}
-
-func SettingsPage(c *fiber.Ctx) error {
-	current_user := getUserFromStore(c)
-
-	return c.Render("settings", fiber.Map{"user": current_user}, "main")
-}
diff --git a/vingo/handlers/scans.go b/vingo/handlers/scans.go
index 670b387..bf68858 100644
--- a/vingo/handlers/scans.go
+++ b/vingo/handlers/scans.go
@@ -43,6 +43,7 @@ func ScanRegister(c *fiber.Ctx) error {
 			logger.Println(err)
 			return c.Status(500).SendString("Error registering card")
 		}
+		registering_success = true
 		return c.SendString("Card registered")
 	}
 
diff --git a/vingo/handlers/settings.go b/vingo/handlers/settings.go
index 99f26a6..dbbeb53 100644
--- a/vingo/handlers/settings.go
+++ b/vingo/handlers/settings.go
@@ -6,21 +6,16 @@ import (
 	"github.com/gofiber/fiber/v2"
 )
 
-func SettingsUpdate(c *fiber.Ctx) error {
-	user := getUserFromStore(c)
-
-	scan_in_out := c.FormValue("scan_in_out")
-	leaderboard := c.FormValue("leaderboard")
-	public := c.FormValue("public")
+type Settings struct{}
 
-	if scan_in_out == "" || leaderboard == "" || public == "" {
-		return c.Status(400).SendString("Missing fields")
-	}
+func (Settings) Update(c *fiber.Ctx) error {
+	user := getUserFromStore(c)
 
-	settings := database.Settings{
-		ScanInOut:   scan_in_out == "on",
-		Leaderboard: leaderboard == "on",
-		Public:      public == "on",
+	settings := database.Settings{}
+	err := c.BodyParser(&settings)
+	if err != nil {
+		logger.Println(err)
+		return c.Status(400).SendString("Invalid payload")
 	}
 
 	sess, _ := store.Get(c)
@@ -29,16 +24,10 @@ func SettingsUpdate(c *fiber.Ctx) error {
 	sess.Set(STORE_USER, &user)
 	sess.Save()
 
-	return c.Redirect("/settings")
+	return c.SendStatus(200)
 }
 
-func Settings(c *fiber.Ctx) error {
+func (Settings) Get(c *fiber.Ctx) error {
 	user := getUserFromStore(c)
-	settings, err := database.GetSettings(user.Id)
-	if err != nil {
-		logger.Println(err)
-		return c.Status(500).SendString("Error getting settings")
-	}
-
-	return c.JSON(settings)
+	return c.JSON(user.Settings)
 }
diff --git a/vingo/handlers/store.go b/vingo/handlers/store.go
index 2f20560..d611854 100644
--- a/vingo/handlers/store.go
+++ b/vingo/handlers/store.go
@@ -15,8 +15,9 @@ var (
 	logger = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile)
 
 	// State for registering a new card
-	registering_user = 0
-	registering_end  = time.Now()
+	registering_user    = 0
+	registering_end     = time.Now()
+	registering_success = false
 )
 
 const (
@@ -34,12 +35,12 @@ func getUserFromStore(c *fiber.Ctx) *database.User {
 	}
 
 	user := sess.Get(STORE_USER)
-	logger.Println("User from store:", user)
 	if user == nil {
 		return nil
 	}
 
 	databaseUser := user.(database.User)
+	logger.Println("User from store:", databaseUser.Id)
 	return &databaseUser
 }
 
diff --git a/vingo/layouts/cards.html b/vingo/layouts/cards.html
deleted file mode 100644
index db2ed35..0000000
--- a/vingo/layouts/cards.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<div class="box">
-    <form action="/cards/register" method="post">
-        <div>
-            If you don't have any cards yet, click the button below to start registering a new card.<br/>
-            Once clicked, you will have 1 minute to scan the card you want to use at the scanner and it will be linked to your account.
-        </div>
-        <div>
-            {{ if .registering }}
-                {{ if .reg_user }}
-                    <input class="warning" disabled type="submit" value="You are registering a card!">
-                {{ else }}
-                    <input class="danger" disabled type="submit" value="Somebody is already registering a card">
-                {{ end }}
-            {{ else }}
-                <input class="success" type="submit" value="Start registering a new card">
-            {{ end }}
-        </div>
-    </form>
-</div>
-<div class="box">
-    {{ if .cards }}
-        <table>
-            <thead>
-                <tr>
-                    <th>card</th>
-                    <th>created_at</th>
-                </tr>
-            </thead>
-        {{ range .cards }}
-            <tr>
-                <td>{{ .Serial }}</td>
-                <td>{{ .CreatedAt.Local.Format "2 January 2006 15:04:05" }}</td>
-            </tr>
-        {{ end }}
-        </table>
-    {{ else }}
-        <h1>No cards yet!</h1>
-    {{ end }}
-</div>
\ No newline at end of file
diff --git a/vingo/layouts/days.html b/vingo/layouts/days.html
deleted file mode 100644
index 87be5ef..0000000
--- a/vingo/layouts/days.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<div class="box">
-    <form action="/days" method="post">
-        <label>
-            Start date:
-            <input type="date" name="start_date"/>
-        </label>
-
-        <label>
-            End date:
-            <input type="date" name="end_date"/>
-        </label>
-
-        <input type="submit" value="Add dates">
-    </form>
-</div>
-<div class="box">
-    {{ if .days }}
-        <table>
-            <thead>
-                <tr>
-                    <th>date</th>
-                    <th>delete</th>
-                </tr>
-            </thead>
-            {{ range .days }}
-                <tr>
-                    <td>{{ .Date.Format "Mon 2 January 2006" }}</td>
-                    <td>
-                        <form action="/days/{{ .Id }}" method="post">
-                            <input type="submit" class="danger" value="Delete">
-                      </form>
-                    </td>
-                </tr>
-            {{ end }}
-        </table>
-    {{ else }}
-        <h1>No days yet!</h1>
-    {{ end }}
-</div>
\ No newline at end of file
diff --git a/vingo/layouts/landing.html b/vingo/layouts/landing.html
deleted file mode 100644
index 588e664..0000000
--- a/vingo/layouts/landing.html
+++ /dev/null
@@ -1 +0,0 @@
-<h1>Ono you are not logged in!</h1>
\ No newline at end of file
diff --git a/vingo/layouts/leaderboard.html b/vingo/layouts/leaderboard.html
deleted file mode 100644
index 5925445..0000000
--- a/vingo/layouts/leaderboard.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<div class="box">
-    <table>
-        <thead>
-            <tr>
-                <th>#</th>
-                <th>Name</th>
-                <th>Total days scanned</th>
-            </tr>
-        </thead>
-        {{ range .leaderboard }}
-            <tr>
-                <td>{{.Position}}</td>
-                <td>{{.Username}}</td>
-                <td>{{.TotalDays}}</td>
-            </tr>
-        {{ end }}
-    </table>
-</div>
\ No newline at end of file
diff --git a/vingo/layouts/main.html b/vingo/layouts/main.html
deleted file mode 100644
index a1719f4..0000000
--- a/vingo/layouts/main.html
+++ /dev/null
@@ -1,171 +0,0 @@
-<!DOCTYPE html>
-<html lang="en" data-theme="light">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>ZeSS</title>
-    <style>
-        :root {
-            font-family: sans-serif;
-            --color-1: #ffb70f;
-            --color-success: #1edb14;
-            --color-warning: #ffb70f;
-            --color-danger: #f42121;
-            --color-inactive: rgb(69, 69, 69);
-            --color-text: black;
-            --color-shadow: #f0f0f0;
-            --color-separator: #d9d9e0;
-        }
-
-        /* Restyle basic elements */
-
-        body {
-            margin: 0px;
-        }
-
-        a {
-            color: var(--color-text);
-            text-decoration: none;
-        }
-
-        input[type=submit] {
-            margin-top: 4px;
-            margin-bottom: 4px;
-            padding: 8px;
-            box-shadow: 0px 4px 8px 0px var(--color-shadow);
-            border: 1px solid var(--color-shadow);
-            border-radius: 6px;
-            background-color: white;
-            font-size: medium;
-            transition-duration: 125ms;
-            transition-property: filter;
-            cursor: pointer;
-        }
-        input[type=submit]:hover {
-            filter: brightness(95%);
-        }
-        input[type=submit]:disabled {
-            filter: opacity(75%);
-        }
-
-        table {
-            width: 100%;
-            border-collapse: collapse;
-        }
-        thead tr {
-            border-bottom: 2px solid var(--color-separator);
-        }
-        tbody tr {
-            border-bottom: 1px solid var(--color-separator);
-            transition-duration: 125ms;
-            transition-property: backdrop-filter;
-        }
-        tbody tr:nth-child(even) {
-            backdrop-filter: brightness(95%);
-        }
-        tbody tr:hover {
-            backdrop-filter: brightness(90%);
-        }
-        th, td {
-            text-align: start;
-            padding: 8px;
-        }
-
-        /* Layout classes */
-
-        div.content {
-            margin: 16px 6vw;
-        }
-        div.content > *:first-child {
-            margin-top: 0px;
-        }
-
-        div.box {
-            margin: 16px;
-            padding: 16px;
-            border-radius: 8px;
-            box-shadow: 0px 4px 8px 0px var(--color-shadow);
-        }
-
-        .push-to-end {
-            margin-inline-start: auto;
-        }
-
-        @media only screen and (max-width: 768px) {
-            div.content {
-                margin: 8px;
-            }
-
-            div.box {
-                margin: 8px;
-                padding: 8px;
-            }
-
-            .hide-mobile {
-                display: none;
-            }
-        }
-
-        /* Color classes */
-
-        .color-1 {
-            background-color: var(--color-1) !important;
-        }
-        .success {
-            background-color: var(--color-success) !important;
-        }
-        .warning {
-            background-color: var(--color-warning) !important;
-        }
-        .danger {
-            background-color: var(--color-danger) !important;
-        }
-        .inactive {
-            background-color: var(--color-inactive) !important;
-        }
-
-        /* Style navbar */
-        nav {
-            display: flex;
-            overflow: auto;
-            text-wrap: nowrap;
-            background-color: var(--color-1);
-            box-shadow: 0 2px 0 0 var(--color-shadow);
-            padding: 0px 6vw;
-        }
-        nav * {
-            display: flex;
-        }
-        nav > div > * {
-            margin: 0px;
-            padding: 10px;
-            line-height: 32px;
-        }
-        nav > div > a {
-            transition-duration: 250ms;
-            transition-property: backdrop-filter;
-        }
-        nav > div > a:hover {
-            backdrop-filter: brightness(90%);
-        }
-        nav > div > a > * {
-            margin-left: 4px;
-        }
-        nav > div > a > *:first-child {
-            margin-left: 0px;
-        }
-        @media only screen and (max-width: 768px) {
-            nav {
-                padding: 0px 0px;
-            }
-        }
-    </style>
-</head>
-<body>
-    <script>0</script> <!-- Make firefox load css before showing page -->
-    {{ template "partials/navbar" . }}
-    <div class="content">
-        {{ embed }}
-    </div>
-</body>
-</html>
diff --git a/vingo/layouts/partials/navbar.html b/vingo/layouts/partials/navbar.html
deleted file mode 100644
index 84ce78d..0000000
--- a/vingo/layouts/partials/navbar.html
+++ /dev/null
@@ -1,43 +0,0 @@
-<nav>
-    <div>
-        <a href="/">
-            <svg width="32px" height="32px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M3 20.4V3.6C3 3.26863 3.26863 3 3.6 3H20.4C20.7314 3 21 3.26863 21 3.6V20.4C21 20.7314 20.7314 21 20.4 21H3.6C3.26863 21 3 20.7314 3 20.4Z" stroke="#000000" stroke-width="1.5"></path><path d="M7.5 8C7.22386 8 7 7.77614 7 7.5C7 7.22386 7.22386 7 7.5 7C7.77614 7 8 7.22386 8 7.5C8 7.77614 7.77614 8 7.5 8Z" fill="#000000" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M16.5 8C16.2239 8 16 7.77614 16 7.5C16 7.22386 16.2239 7 16.5 7C16.7761 7 17 7.22386 17 7.5C17 7.77614 16.7761 8 16.5 8Z" fill="#000000" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7.5 12.5C7.22386 12.5 7 12.2761 7 12C7 11.7239 7.22386 11.5 7.5 11.5C7.77614 11.5 8 11.7239 8 12C8 12.2761 7.77614 12.5 7.5 12.5Z" fill="#000000" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M16.5 12.5C16.2239 12.5 16 12.2761 16 12C16 11.7239 16.2239 11.5 16.5 11.5C16.7761 11.5 17 11.7239 17 12C17 12.2761 16.7761 12.5 16.5 12.5Z" fill="#000000" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5C8 16.7761 7.77614 17 7.5 17Z" fill="#000000" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M16.5 17C16.2239 17 16 16.7761 16 16.5C16 16.2239 16.2239 16 16.5 16C16.7761 16 17 16.2239 17 16.5C17 16.7761 16.7761 17 16.5 17Z" fill="#000000" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
-            <span class="hide-mobile">ZeSS</span>
-        </a>
-        {{ if .user }}
-            <a href="/scans">
-                <svg width="32px" height="32px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M10 12L10 6L11 6" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M10 12L11 12L11 6" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M10 18L10 15L11 15" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M11 15L11 18H10" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 6L7 12" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 15L7 18" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 6L14 12" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 15L14 18" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M17 6L17 12" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M17 15L17 18" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 3H3V6" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M2 12H12L22 12" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M18 3H21V6" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 21H3V18" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M18 21H21V18" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
-                <span class="hide-mobile">Scans</span>
-            </a>
-            <a href="/cards">
-                <svg width="32px" height="32px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M5 19V3H19V19C19 20.1046 18.1046 21 17 21H7C5.89543 21 5 20.1046 5 19Z" stroke="#000000" stroke-width="1.5"></path><path d="M5 6H3.5C2.67157 6 2 5.32843 2 4.5V4.5C2 3.67157 2.67157 3 3.5 3H20.5C21.3284 3 22 3.67157 22 4.5V4.5C22 5.32843 21.3284 6 20.5 6H19" stroke="#000000" stroke-width="1.5"></path><path d="M15 3L15 21" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
-                <span class="hide-mobile">Cards</span>
-            </a>
-            <a href="/leaderboard">
-                <svg width="32px" height="32px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M15 21H9V12.6C9 12.2686 9.26863 12 9.6 12H14.4C14.7314 12 15 12.2686 15 12.6V21Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20.4 21H15V18.1C15 17.7686 15.2686 17.5 15.6 17.5H20.4C20.7314 17.5 21 17.7686 21 18.1V20.4C21 20.7314 20.7314 21 20.4 21Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M9 21V16.1C9 15.7686 8.73137 15.5 8.4 15.5H3.6C3.26863 15.5 3 15.7686 3 16.1V20.4C3 20.7314 3.26863 21 3.6 21H9Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M10.8056 5.11325L11.7147 3.1856C11.8314 2.93813 12.1686 2.93813 12.2853 3.1856L13.1944 5.11325L15.2275 5.42427C15.4884 5.46418 15.5923 5.79977 15.4035 5.99229L13.9326 7.4917L14.2797 9.60999C14.3243 9.88202 14.0515 10.0895 13.8181 9.96099L12 8.96031L10.1819 9.96099C9.94851 10.0895 9.67568 9.88202 9.72026 9.60999L10.0674 7.4917L8.59651 5.99229C8.40766 5.79977 8.51163 5.46418 8.77248 5.42427L10.8056 5.11325Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
-                <span class="hide-mobile">Leaderboard</span>
-            </a>
-            {{ if .user.Admin }}
-                <a class="danger" href="/days">
-                    <svg width="32px" height="32px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000" style="--darkreader-inline-color: #e8e6e3;" data-darkreader-inline-color=""><path d="M4 19V5C4 3.89543 4.89543 3 6 3H19.4C19.7314 3 20 3.26863 20 3.6V16.7143" stroke="#000000" stroke-width="1.5" stroke-linecap="round" style="--darkreader-inline-stroke: #000000;" data-darkreader-inline-stroke=""></path><path d="M14 10H14.4C14.7314 10 15 10.2686 15 10.6V13.4C15 13.7314 14.7314 14 14.4 14H9.6C9.26863 14 9 13.7314 9 13.4V10.6C9 10.2686 9.26863 10 9.6 10H10M14 10V8C14 7.33333 13.6 6 12 6C10.4 6 10 7.33333 10 8V10M14 10H10" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="--darkreader-inline-stroke: #000000;" data-darkreader-inline-stroke=""></path><path d="M6 17L20 17" stroke="#000000" stroke-width="1.5" stroke-linecap="round" style="--darkreader-inline-stroke: #000000;" data-darkreader-inline-stroke=""></path><path d="M6 21L20 21" stroke="#000000" stroke-width="1.5" stroke-linecap="round" style="--darkreader-inline-stroke: #000000;" data-darkreader-inline-stroke=""></path><path d="M6 21C4.89543 21 4 20.1046 4 19C4 17.8954 4.89543 17 6 17" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="--darkreader-inline-stroke: #000000;" data-darkreader-inline-stroke=""></path></svg>
-                    <span class="hide-mobile">Admin</span>
-                </a>
-            {{ end }}
-        {{ end }}
-    </div>
-    <div class="push-to-end">
-        {{ if .user }}
-            <div class="hide-mobile">Hello {{ .user.Username }}!</div>
-            <a href="/settings">
-                <svg width="32px" height="32px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19.6224 10.3954L18.5247 7.7448L20 6L18 4L16.2647 5.48295L13.5578 4.36974L12.9353 2H10.981L10.3491 4.40113L7.70441 5.51596L6 4L4 6L5.45337 7.78885L4.3725 10.4463L2 11V13L4.40111 13.6555L5.51575 16.2997L4 18L6 20L7.79116 18.5403L10.397 19.6123L11 22H13L13.6045 19.6132L16.2551 18.5155C16.6969 18.8313 18 20 18 20L20 18L18.5159 16.2494L19.6139 13.598L21.9999 12.9772L22 11L19.6224 10.3954Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
-            </a>
-            <a href="/logout">
-                <svg width="32px" height="32px" viewBox="0 0 24 24" stroke-width="1.5" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M12 12H19M19 12L16 15M19 12L16 9" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M19 6V5C19 3.89543 18.1046 3 17 3H7C5.89543 3 5 3.89543 5 5V19C5 20.1046 5.89543 21 7 21H17C18.1046 21 19 20.1046 19 19V18" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
-            </a>
-        {{ else }}
-            <a href="/login">
-                <span>Login with Zauth :&rpar;</span>
-            </a>
-        {{ end }}
-    </div>
-</nav>
diff --git a/vingo/layouts/scans.html b/vingo/layouts/scans.html
deleted file mode 100644
index 5804ef6..0000000
--- a/vingo/layouts/scans.html
+++ /dev/null
@@ -1,20 +0,0 @@
-<div class="box">
-    {{ if .scans }}
-        <table>
-            <thead>
-                <tr>
-                    <th>scan_time</th>
-                    <th>card</th>
-                </tr>
-            </thead>
-            {{ range .scans }}
-                <tr>
-                    <td>{{ .ScanTime.Local.Format "Mon 2 January 2006 15:04:05" }}</td>
-                    <td>{{ .Card }}</td>
-                </tr>
-            {{ end }}
-        </table>
-    {{ else }}
-        <div class="title">No scans yet!</div>
-    {{ end }}
-</div>
\ No newline at end of file
diff --git a/vingo/layouts/settings.html b/vingo/layouts/settings.html
deleted file mode 100644
index 9173a9c..0000000
--- a/vingo/layouts/settings.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<h1 class="title">
-    Yay! Settings! They don't do anything :(
-</h1>
-
-<form class="settings" action="/settings" method="post" autocomplete="off">
-    <label>
-        <input type="checkbox" name="scan_in_out" {{ if .user.Settings.ScanInOut }} checked {{ end }}>
-        <input type='hidden' value='off' name='scan_in_out'>
-        Keep track of time based on scan-in and scan-out (experimental)
-    </label>
-    <label>
-        <input type="checkbox" name="leaderboard" {{ if .user.Settings.Leaderboard }} checked {{ end }}>
-        <input type='hidden' value='off' name='leaderboard'>
-        Show on leaderboard
-    </label>
-    <label>
-        <input type="checkbox" name="public" {{ if .user.Settings.Public }} checked {{ end }}>
-        <input type='hidden' value='off' name='public'>
-        (Semi-)Public profile
-    </label>
-    <input type="submit" value="Save settings">
-</form>
-
-<style>
-    .settings {
-        display: flex;
-        flex-direction: column;
-    }
-</style>
\ No newline at end of file
diff --git a/vingo/layouts/stats.html b/vingo/layouts/stats.html
deleted file mode 100644
index dbf37ce..0000000
--- a/vingo/layouts/stats.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<style>
-    .weekBox {
-        display: flex;
-        flex-direction: row-reverse;
-        justify-content: space-between;
-        overflow: auto;
-    }
-
-    .weekBox > div {
-        height: 80px;
-        width: 80px;
-        border-radius: 20px;
-
-        display: flex;
-        align-items: center;
-        justify-content: center;
-
-        color: white;
-        font-weight: bolder;
-        text-wrap: nowrap;
-    }
-</style>
-
-<div class="box weekBox">
-    {{ range .days_present_7 }}
-        <div class="box {{ if .Present }} success {{ else if .StreakDay }} danger {{ else }} inactive {{ end }}">
-            {{ .Date.Format "Mon 02 Jan" }}
-        </div>
-    {{ end }}
-</div>
\ No newline at end of file
diff --git a/vingo/main.go b/vingo/main.go
index fc0301b..ab8e540 100644
--- a/vingo/main.go
+++ b/vingo/main.go
@@ -10,7 +10,6 @@ import (
 
 	"github.com/gofiber/fiber/v2"
 	"github.com/gofiber/fiber/v2/middleware/cors"
-	"github.com/gofiber/template/html/v2"
 	"github.com/joho/godotenv"
 	_ "github.com/lib/pq"
 )
@@ -26,10 +25,7 @@ func main() {
 	db := database.Get()
 	defer db.Close()
 
-	engine := html.New("./layouts", ".html")
-	public := fiber.New(fiber.Config{
-		Views: engine,
-	})
+	public := fiber.New(fiber.Config{})
 
 	public.Use(cors.New(cors.Config{
 		AllowOrigins:     corsAllowOrigins,
@@ -38,44 +34,31 @@ func main() {
 	}))
 
 	// Public routes
-	public.Get("/", handlers.Index)
-
-	public.Get("/login", handlers.Login)
+	public.Post("/login", handlers.Login)
 	public.Get("/auth/callback", handlers.Callback)
 
 	public.Post("/scans", handlers.ScanRegister)
 
-	// Logged in routes
-	logged := public.Group("/", handlers.IsLoggedIn)
-	{
-		logged.Get("/logout", handlers.Logout)
-
-		logged.Get("/scans", handlers.ScansPage)
-
-		logged.Get("/cards", handlers.CardsPage)
-		logged.Post("/cards/register", handlers.StartCardRegister)
-
-		logged.Get("/leaderboard", handlers.LeaderboardPage)
-
-		logged.Get("/settings", handlers.Settings)
-		logged.Post("/settings", handlers.SettingsUpdate)
-	}
-
-	api := logged.Group("/api", handlers.IsLoggedInAPI)
+	api := public.Group("/api", handlers.IsLoggedIn)
 	{
+		api.Post("/logout", handlers.Logout)
 		api.Get("/user", handlers.User)
 		api.Get("/leaderboard", handlers.Leaderboard)
 		api.Get("/scans", handlers.Scans)
-		api.Get("/cards", handlers.Cards)
-		api.Get("/settings", handlers.Settings)
-	}
 
-	// Admin routes
-	admin := logged.Group("/", handlers.IsAdmin)
-	{
-		admin.Get("/days", handlers.DaysPage)
-		admin.Post("/days", handlers.DaysRegister)
-		admin.Post("/days/:id", handlers.DaysDelete)
+		api.Get("/cards", handlers.Cards{}.Get)
+		api.Patch("/cards/:id", handlers.Cards{}.Update)
+		api.Get("/cards/register", handlers.Cards{}.RegisterStatus)
+		api.Post("/cards/register", handlers.Cards{}.StartRegister)
+
+		api.Get("/settings", handlers.Settings{}.Get)
+		api.Patch("/settings", handlers.Settings{}.Update)
+
+		admin := api.Group("/admin", handlers.IsAdmin)
+		{
+			admin.Post("/days", handlers.DaysRegister)
+			admin.Post("/days/:id", handlers.DaysDelete)
+		}
 	}
 
 	log.Println(public.Listen(":4000"))
diff --git a/vinvoor/index.html b/vinvoor/index.html
index 1158735..74c815e 100644
--- a/vinvoor/index.html
+++ b/vinvoor/index.html
@@ -2,7 +2,7 @@
 <html lang="en">
     <head>
         <meta charset="UTF-8" />
-        <link rel="icon" type="image/svg+xml" href="/zeus.svg" />
+        <link rel="icon" type="image/svg+xml" href="/logo.svg" />
         <meta name="viewport" content="width=device-width, initial-scale=1.0" />
         <title>ZeSS</title>
     </head>
diff --git a/vinvoor/package.json b/vinvoor/package.json
index 2f17aa0..fdfbba9 100644
--- a/vinvoor/package.json
+++ b/vinvoor/package.json
@@ -19,12 +19,18 @@
     "@types/js-cookie": "^3.0.6",
     "@types/react-router-dom": "^5.3.3",
     "@types/react-router-hash-link": "^2.4.9",
+    "apexcharts": "^3.50.0",
     "js-cookie": "^3.0.5",
+    "material-ui-confirm": "^3.0.16",
     "mdi-material-ui": "^7.9.1",
+    "notistack": "^3.0.1",
     "react": "^18.2.0",
+    "react-apexcharts": "^1.4.1",
+    "react-device-detect": "^2.2.3",
     "react-dom": "^18.2.0",
     "react-router-dom": "^6.23.1",
-    "react-router-hash-link": "^2.4.3"
+    "react-router-hash-link": "^2.4.3",
+    "react-tooltip": "^5.27.0"
   },
   "devDependencies": {
     "@types/react": "^18.2.66",
@@ -36,6 +42,7 @@
     "eslint-plugin-react-hooks": "^4.6.0",
     "eslint-plugin-react-refresh": "^0.4.6",
     "typescript": "^5.2.2",
-    "vite": "^5.2.0"
+    "vite": "^5.2.0",
+    "vite-plugin-svgr": "^4.2.0"
   }
 }
diff --git a/vinvoor/public/logo.svg b/vinvoor/public/logo.svg
new file mode 100644
index 0000000..f634a40
--- /dev/null
+++ b/vinvoor/public/logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>hexagon-slice-6</title><path fill="#ff7f00" d="M12,5.32L18,8.69V15.31L12,18.68L6,15.31V8.69L12,5.32M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L5,8.09V15.91L12,19.85L19,15.91V8.09L12,4.15Z" /></svg>
diff --git a/vinvoor/src/App.tsx b/vinvoor/src/App.tsx
index 8371a6c..76278d2 100644
--- a/vinvoor/src/App.tsx
+++ b/vinvoor/src/App.tsx
@@ -1,8 +1,9 @@
 import { Container } from "@mui/material";
 import { useContext } from "react";
-import { Navigate, Outlet } from "react-router-dom";
+import { Navigate, Outlet, useOutlet } from "react-router-dom";
 import { LoadingSkeleton } from "./components/LoadingSkeleton";
 import { NavBar } from "./navbar/NavBar";
+import { Overview } from "./overview/Overview";
 import { UserContext } from "./user/UserProvider";
 import { WelcomePage } from "./WelcomePage";
 
@@ -11,13 +12,19 @@ export const App = () => {
         userState: { user, loading },
     } = useContext(UserContext);
 
+    const outlet = useOutlet();
+
     return (
         <>
             <NavBar />
             <Container maxWidth="xl" sx={{ my: "2%" }}>
                 <LoadingSkeleton loading={loading}>
                     {user !== undefined ? (
-                        <Outlet />
+                        outlet !== null ? (
+                            <Outlet />
+                        ) : (
+                            <Overview />
+                        )
                     ) : (
                         <>
                             <WelcomePage />
@@ -29,5 +36,3 @@ export const App = () => {
         </>
     );
 };
-
-// TODO: Add link to the github repo
diff --git a/vinvoor/src/WelcomePage.tsx b/vinvoor/src/WelcomePage.tsx
index c8cd2f7..e4a7e28 100644
--- a/vinvoor/src/WelcomePage.tsx
+++ b/vinvoor/src/WelcomePage.tsx
@@ -2,7 +2,7 @@ import { GitHub } from "@mui/icons-material";
 import { Box, Button, Typography } from "@mui/material";
 import { ShakerOutline } from "mdi-material-ui";
 import { TypographyG } from "./components/TypographyG";
-import { UnstyledLink } from "./components/UnstyledLink";
+import { Login } from "./user/Login";
 
 declare module "@mui/material/Button" {
     interface ButtonPropsColorOverrides {
@@ -29,12 +29,10 @@ export const WelcomePage = () => {
         >
             <TypographyG variant="h3">Welcome to Vinvoor!</TypographyG>
             <TypographyG variant="h4">Log in to start scanning</TypographyG>
-            <UnstyledLink to="/login">
-                <Button variant="contained">
-                    <Typography>Log in with Zauth</Typography>
-                    <ShakerOutline sx={{ ml: 1 }} />
-                </Button>
-            </UnstyledLink>
+            <Login variant="contained">
+                <Typography>Log in with Zauth</Typography>
+                <ShakerOutline sx={{ ml: 1 }} />
+            </Login>
             <Button
                 variant="contained"
                 color="github"
@@ -47,3 +45,10 @@ export const WelcomePage = () => {
         </Box>
     );
 };
+
+// <UnstyledLink to="/login">
+//     <Button variant="contained">
+//         <Typography>Log in with Zauth</Typography>
+//         <ShakerOutline sx={{ ml: 1 }} />
+//     </Button>
+// </UnstyledLink>
diff --git a/vinvoor/src/cards/Cards.tsx b/vinvoor/src/cards/Cards.tsx
index fa2c699..e4e00c4 100644
--- a/vinvoor/src/cards/Cards.tsx
+++ b/vinvoor/src/cards/Cards.tsx
@@ -1,21 +1,33 @@
-import { useState } from "react";
+import { createContext, Dispatch, SetStateAction, useState } from "react";
 import { LoadingSkeleton } from "../components/LoadingSkeleton";
 import { useFetch } from "../hooks/useFetch";
-import { Card } from "../types/cards";
+import { Card, convertCardJSON } from "../types/cards";
 import { CardsEmpty } from "./CardsEmpty";
 import { CardsTable } from "./CardsTable";
 
+interface CardContextProps {
+    cards: readonly Card[];
+    setCards: Dispatch<SetStateAction<readonly Card[]>>;
+}
+
+export const CardContext = createContext<CardContextProps>({
+    cards: [],
+    setCards: () => {},
+});
+
 export const Cards = () => {
     const [cards, setCards] = useState<readonly Card[]>([]);
-    const { loading, error: _ } = useFetch<readonly Card[]>("cards", setCards);
+    const { loading } = useFetch<readonly Card[]>(
+        "cards",
+        setCards,
+        convertCardJSON
+    );
 
     return (
         <LoadingSkeleton loading={loading}>
-            {!!cards.length ? (
-                <CardsTable cards={cards} setCards={setCards} />
-            ) : (
-                <CardsEmpty />
-            )}
+            <CardContext.Provider value={{ cards, setCards }}>
+                {!!cards.length ? <CardsTable /> : <CardsEmpty />}
+            </CardContext.Provider>
         </LoadingSkeleton>
     );
 };
diff --git a/vinvoor/src/cards/CardsAdd.tsx b/vinvoor/src/cards/CardsAdd.tsx
index 1eb4b9b..fa5eb00 100644
--- a/vinvoor/src/cards/CardsAdd.tsx
+++ b/vinvoor/src/cards/CardsAdd.tsx
@@ -1,47 +1,157 @@
-import { Add, CancelOutlined } from "@mui/icons-material";
+import { Add } from "@mui/icons-material";
 import { Button, Typography } from "@mui/material";
-import { useState } from "react";
-import { ConfirmationModal } from "../components/ConfirmationModal";
+import { useConfirm } from "material-ui-confirm";
+import { useSnackbar } from "notistack";
+import { useContext, useEffect, useState } from "react";
+import {
+    Card,
+    CardGetRegisterResponse,
+    CardPostResponse,
+    convertCardJSON,
+} from "../types/cards";
+import { getApi, isResponseNot200Error, postApi } from "../util/fetch";
+import { randomInt } from "../util/util";
+import { CardContext } from "./Cards";
+import {
+    CircularTimeProgress,
+    CircularTimeProgressProps,
+} from "./CircularTimeProgress";
+
+const CHECK_INTERVAL = 1000;
+const REGISTER_TIME = 60000;
+const REGISTER_ENDPOINT = "cards/register";
+
+const defaultProgressProps: CircularTimeProgressProps = {
+    time: REGISTER_TIME,
+    percentage: 1,
+};
+
+const confirmTitle = "Register a new card";
+const confirmContent = `
+        Once you click 'register' you will have 60 seconds to hold your card to the scanner.
+        A popup will appear when the card is registered successfully and it will be added to your cards table.
+    `;
+
+const requestSuccess = "Register your card by holding it to vinscant";
+const requestYou = "You are already registering a card!";
+const requestOther =
+    "Failed to start the card registering process because another user is already registering a card. Please try again later.";
+const requestFail =
+    "Failed to start the card registration process. Please try again later or contact a sysadmin";
+
+const registerSucces = "Card registered successfully";
+const registerFail = "Failed to register card";
 
 export const CardsAdd = () => {
-    const [open, setOpen] = useState<boolean>(false);
+    const { setCards } = useContext(CardContext);
+    const [registering, setRegistering] = useState<boolean>(false);
+    const [progressProps, setProgressProps] =
+        useState<CircularTimeProgressProps>(defaultProgressProps);
+    const confirm = useConfirm();
+    const { enqueueSnackbar, closeSnackbar } = useSnackbar();
 
-    const handleOpen = () => setOpen(true);
-    const handleClose = () => setOpen(false);
+    const checkCardsChange = async (): Promise<boolean> => {
+        let status: CardGetRegisterResponse =
+            await getApi<CardGetRegisterResponse>(REGISTER_ENDPOINT);
+        while (status.registering && status.isCurrentUser) {
+            setProgressProps({
+                time: status.timeRemaining,
+                percentage: status.timePercentage,
+            });
+            status = await getApi<CardGetRegisterResponse>(REGISTER_ENDPOINT);
+            await new Promise((r) => setTimeout(r, CHECK_INTERVAL));
+        }
 
-    const title = "Register a new card";
+        return status.success;
+    };
 
-    const content = `
-        This feature is not yet implemented as I'm waiting for an endpoint.
-        Hannes................................................
-    `;
+    const handleRegister = (start: boolean) => {
+        getApi<CardGetRegisterResponse>(REGISTER_ENDPOINT)
+            .then(async (response) => {
+                let started = false;
+                if (!response.registering && start) {
+                    await postApi<CardPostResponse>(REGISTER_ENDPOINT)
+                        .then(() => (started = true))
+                        .catch((error) => {
+                            if (isResponseNot200Error(error))
+                                error.response
+                                    .json()
+                                    .then((response: CardPostResponse) => {
+                                        if (response.isCurrentUser)
+                                            enqueueSnackbar(requestYou, {
+                                                variant: "warning",
+                                            });
+                                        else
+                                            enqueueSnackbar(requestOther, {
+                                                variant: "error",
+                                            });
+                                    });
+                            else throw new Error(error);
+                        });
+                }
 
-    const actions = (
-        <>
-            <Button onClick={handleClose} color="error" variant="contained">
-                <CancelOutlined sx={{ mr: 1 }} />
-                <Typography>Cancel</Typography>
-            </Button>
-            <Button color="success" variant="contained">
-                <Add sx={{ mr: 1 }} />
-                <Typography>Register</Typography>
-            </Button>
-        </>
-    );
+                if (response.registering && response.isCurrentUser)
+                    started = true;
+
+                if (started) {
+                    setRegistering(true);
+                    const id = randomInt().toString();
+                    enqueueSnackbar(requestSuccess, {
+                        variant: "info",
+                        persist: true,
+                        key: id,
+                    });
+
+                    checkCardsChange()
+                        .then((scanned) => {
+                            closeSnackbar(id);
+                            setRegistering(false);
+                            if (scanned) {
+                                enqueueSnackbar(registerSucces, {
+                                    variant: "success",
+                                });
+                                getApi<readonly Card[]>(
+                                    "cards",
+                                    convertCardJSON
+                                ).then((cards) => setCards(cards));
+                            } else
+                                enqueueSnackbar(registerFail, {
+                                    variant: "error",
+                                });
+                        })
+                        .finally(() => setProgressProps(defaultProgressProps));
+                }
+            })
+            .catch(() => enqueueSnackbar(requestFail, { variant: "error" }));
+    };
+
+    const handleClick = () => {
+        confirm({
+            title: confirmTitle,
+            description: confirmContent,
+            confirmationText: "Register",
+        })
+            .then(() => handleRegister(true))
+            .catch(() => {}); // Required otherwise the confirm dialog will throw an error in the console
+    };
+
+    useEffect(() => {
+        handleRegister(false);
+    }, []);
 
     return (
-        <>
-            <Button onClick={handleOpen} variant="contained" sx={{ my: "1%" }}>
+        <Button
+            onClick={handleClick}
+            variant="contained"
+            sx={{ my: "1%" }}
+            disabled={registering}
+        >
+            {registering ? (
+                <CircularTimeProgress {...progressProps} />
+            ) : (
                 <Add />
-                <Typography>Register new card</Typography>
-            </Button>
-            <ConfirmationModal
-                open={open}
-                onClose={handleClose}
-                title={title}
-                content={content}
-                actions={actions}
-            ></ConfirmationModal>
-        </>
+            )}
+            <Typography>Register new card</Typography>
+        </Button>
     );
 };
diff --git a/vinvoor/src/cards/CardsDelete.tsx b/vinvoor/src/cards/CardsDelete.tsx
index df7e274..fa63fcd 100644
--- a/vinvoor/src/cards/CardsDelete.tsx
+++ b/vinvoor/src/cards/CardsDelete.tsx
@@ -1,61 +1,39 @@
-import { CancelOutlined } from "@mui/icons-material";
 import DeleteIcon from "@mui/icons-material/Delete";
-import { Button, IconButton, Tooltip, Typography } from "@mui/material";
-import { Dispatch, FC, SetStateAction, useState } from "react";
-import { ConfirmationModal } from "../components/ConfirmationModal";
-import { Card } from "../types/cards";
+import { IconButton, Link, Tooltip, Typography } from "@mui/material";
+import { useConfirm } from "material-ui-confirm";
+import { FC } from "react";
 
 interface CardDeleteProps {
     selected: readonly string[];
-    setCards: Dispatch<SetStateAction<readonly Card[]>>;
 }
 
-export const CardsDelete: FC<CardDeleteProps> = ({ selected, setCards }) => {
-    const [open, setOpen] = useState<boolean>(false);
-
+export const CardsDelete: FC<CardDeleteProps> = ({ selected }) => {
+    const confirm = useConfirm();
     const numSelected = selected.length;
 
-    const handleOpen = () => setOpen(true);
-    const handleClose = () => setOpen(false);
-
     const title = `Delete card${numSelected > 1 ? "s" : ""}`;
-
-    const content = `
-        Are you sure you want to delete ${numSelected} card${
-        numSelected > 1 ? "s" : ""
-    }? Unfortunately, this
-        feature isn't implemented yet. Again, I'm waiting
-        for an endpoint.
-        Hannnneeeeeeees...........................
-    `;
-
-    const actions = (
-        <>
-            <Button onClick={handleClose} color="error" variant="contained">
-                <CancelOutlined sx={{ mr: 1 }} />
-                <Typography>Cancel</Typography>
-            </Button>
-            <Button color="warning" variant="contained">
-                <DeleteIcon sx={{ mr: 1 }} />
-                <Typography>Delete</Typography>
-            </Button>
-        </>
+    const content = (
+        <Typography>
+            ` Are you sure you want to delete ${numSelected} card$
+            {numSelected > 1 ? "s" : ""}? Unfortunately, this feature isn't
+            available yet. Let's convince Hannes to add this feature by signing
+            this <Link href="https://chng.it/nQ6GSXVRMJ">petition!</Link>`
+        </Typography>
     );
 
+    const handleClick = () => {
+        confirm({
+            title: title,
+            description: content,
+            confirmationText: "Delete",
+        }).then(() => console.log("Card deleted!"));
+    };
+
     return (
-        <>
-            <Tooltip title="Delete">
-                <IconButton onClick={handleOpen}>
-                    <DeleteIcon />
-                </IconButton>
-            </Tooltip>
-            <ConfirmationModal
-                open={open}
-                onClose={handleClose}
-                title={title}
-                content={content}
-                actions={actions}
-            ></ConfirmationModal>
-        </>
+        <Tooltip title="Delete">
+            <IconButton onClick={handleClick}>
+                <DeleteIcon />
+            </IconButton>
+        </Tooltip>
     );
 };
diff --git a/vinvoor/src/cards/CardsTable.tsx b/vinvoor/src/cards/CardsTable.tsx
index 8c463aa..512e777 100644
--- a/vinvoor/src/cards/CardsTable.tsx
+++ b/vinvoor/src/cards/CardsTable.tsx
@@ -1,33 +1,18 @@
 import { Paper, Table, TableContainer, TablePagination } from "@mui/material";
-import {
-    ChangeEvent,
-    Dispatch,
-    FC,
-    MouseEvent,
-    SetStateAction,
-    useMemo,
-    useState,
-} from "react";
+import { ChangeEvent, MouseEvent, useContext, useMemo, useState } from "react";
 import { Card } from "../types/cards";
-import { TableOrder } from "../types/table";
+import { TableOrder } from "../types/general";
+import { CardContext } from "./Cards";
 import { CardsTableBody } from "./CardsTableBody";
 import { CardsTableHead } from "./CardsTableHead";
 import { CardsTableToolbar } from "./CardsTableToolbar";
 
-interface CardTableProps {
-    cards: readonly Card[];
-    setCards: Dispatch<SetStateAction<readonly Card[]>>;
-}
-
 const rowsPerPageOptions = [10, 25, 50];
 
 const descendingComparator = <T,>(a: T, b: T, orderBy: keyof T) => {
-    if (b[orderBy] < a[orderBy]) {
-        return -1;
-    }
-    if (b[orderBy] > a[orderBy]) {
-        return 1;
-    }
+    if (b[orderBy] < a[orderBy]) return -1;
+    if (b[orderBy] > a[orderBy]) return 1;
+
     return 0;
 };
 
@@ -35,8 +20,8 @@ const getComparator = <Key extends keyof Card>(
     order: TableOrder,
     orderBy: Key
 ): ((
-    a: { [key in Key]: number | string },
-    b: { [key in Key]: number | string }
+    a: { [key in Key]: number | string | Date },
+    b: { [key in Key]: number | string | Date }
 ) => number) => {
     return order === "desc"
         ? (a, b) => descendingComparator(a, b, orderBy)
@@ -47,18 +32,19 @@ const stableSort = <T,>(
     array: readonly T[],
     comparator: (a: T, b: T) => number
 ) => {
-    const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
-    stabilizedThis.sort((a, b) => {
+    const stabilized = array.map((el, index) => [el, index] as [T, number]);
+    stabilized.sort((a, b) => {
         const order = comparator(a[0], b[0]);
         if (order !== 0) {
             return order;
         }
         return a[1] - b[1];
     });
-    return stabilizedThis.map((el) => el[0]);
+    return stabilized.map((el) => el[0]);
 };
 
-export const CardsTable: FC<CardTableProps> = ({ cards, setCards }) => {
+export const CardsTable = () => {
+    const { cards } = useContext(CardContext);
     const [order, setOrder] = useState<TableOrder>("asc");
     const [orderBy, setOrderBy] = useState<keyof Card>("serial");
     const [selected, setSelected] = useState<readonly string[]>([]);
@@ -78,6 +64,7 @@ export const CardsTable: FC<CardTableProps> = ({ cards, setCards }) => {
         if (event.target.checked) {
             const newSelected = cards.map((n) => n.serial);
             setSelected(newSelected);
+
             return;
         }
 
@@ -85,7 +72,7 @@ export const CardsTable: FC<CardTableProps> = ({ cards, setCards }) => {
     };
 
     const handleRowClick = (
-        _: MouseEvent<HTMLTableRowElement>,
+        _: MouseEvent<HTMLTableCellElement>,
         serial: string
     ) => {
         const selectedIndex = selected.indexOf(serial);
@@ -114,9 +101,7 @@ export const CardsTable: FC<CardTableProps> = ({ cards, setCards }) => {
     const handleChangePage = (
         _: MouseEvent<HTMLButtonElement> | null,
         newPage: number
-    ) => {
-        setPage(newPage);
-    };
+    ) => setPage(newPage);
 
     const handleChangeRowsPerPage = (event: ChangeEvent<HTMLInputElement>) => {
         setRowsPerPage(parseInt(event.target.value, 10));
@@ -130,16 +115,16 @@ export const CardsTable: FC<CardTableProps> = ({ cards, setCards }) => {
 
     const visibleRows = useMemo(
         () =>
-            stableSort(cards, getComparator(order, orderBy)).slice(
+            stableSort<Card>(cards, getComparator(order, orderBy)).slice(
                 page * rowsPerPage,
                 page * rowsPerPage + rowsPerPage
             ),
-        [order, orderBy, page, rowsPerPage]
+        [cards, order, orderBy, page, rowsPerPage]
     );
 
     return (
         <Paper elevation={4} sx={{ width: "100%", mb: 2 }}>
-            <CardsTableToolbar selected={selected} setCards={setCards} />
+            <CardsTableToolbar selected={selected} />
             <TableContainer>
                 <Table>
                     <CardsTableHead
diff --git a/vinvoor/src/cards/CardsTableBody.tsx b/vinvoor/src/cards/CardsTableBody.tsx
index 0035265..1d01ef7 100644
--- a/vinvoor/src/cards/CardsTableBody.tsx
+++ b/vinvoor/src/cards/CardsTableBody.tsx
@@ -1,51 +1,113 @@
+import { EditOutlined } from "@mui/icons-material";
 import {
     Checkbox,
+    IconButton,
     TableBody,
     TableCell,
     TableRow,
+    TextField,
     Typography,
 } from "@mui/material";
-import { FC, MouseEvent } from "react";
-import { Card, CardsHeadCells } from "../types/cards";
+import { useConfirm } from "material-ui-confirm";
+import { useSnackbar } from "notistack";
+import { ChangeEvent, FC, MouseEvent, useContext } from "react";
+import { Card, cardsHeadCells, convertCardJSON } from "../types/cards";
+import { getApi, patchApi } from "../util/fetch";
+import { CardContext } from "./Cards";
 
 interface CardsTableBodyProps {
     rows: readonly Card[];
     isRowSelected: (serial: string) => boolean;
     handleClick: (
-        event: MouseEvent<HTMLTableRowElement>,
+        event: MouseEvent<HTMLTableCellElement>,
         serial: string
     ) => void;
     emptyRows: number;
 }
 
+const nameSaveSuccess = "New name saved successfully";
+const nameSaveFailure = "Unable to save new name";
+
 export const CardsTableBody: FC<CardsTableBodyProps> = ({
     rows,
     isRowSelected,
     handleClick,
     emptyRows,
 }) => {
+    const { setCards } = useContext(CardContext);
+    const confirm = useConfirm();
+    const { enqueueSnackbar } = useSnackbar();
+
+    const handleEditClick = (id: number, name: string) => {
+        let newName = name;
+        confirm({
+            title: "Enter new name",
+            content: (
+                <TextField
+                    variant="standard"
+                    defaultValue={name}
+                    onChange={(event: ChangeEvent<HTMLInputElement>) =>
+                        (newName = event.target.value)
+                    }
+                ></TextField>
+            ),
+            confirmationText: "Save",
+        })
+            .then(() => {
+                if (newName === name) {
+                    enqueueSnackbar(nameSaveSuccess, { variant: "success" });
+                    return;
+                }
+
+                patchApi(`cards/${id}`, { name: newName })
+                    .then(() => {
+                        enqueueSnackbar(nameSaveSuccess, {
+                            variant: "success",
+                        });
+                        getApi<readonly Card[]>("cards", convertCardJSON).then(
+                            (cards) => setCards(cards)
+                        );
+                    })
+                    .catch((error) => {
+                        enqueueSnackbar(nameSaveFailure, { variant: "error" });
+                        console.log(error);
+                    });
+            })
+            .catch(() => {}); // Required otherwise the confirm dialog will throw an error in the console
+    };
+
+    const editButton = (id: number, name: string) => (
+        <IconButton onClick={() => handleEditClick(id, name)}>
+            <EditOutlined />
+        </IconButton>
+    );
+
     return (
         <TableBody>
             {rows.map((row) => {
                 const isSelected = isRowSelected(row.serial);
 
                 return (
-                    <TableRow
-                        key={row.serial}
-                        selected={isSelected}
-                        onClick={(event) => handleClick(event, row.serial)}
-                        sx={{ cursor: "pointer" }}
-                    >
-                        <TableCell padding="checkbox">
+                    <TableRow key={row.serial} selected={isSelected}>
+                        <TableCell
+                            onClick={(event) => handleClick(event, row.serial)}
+                            padding="checkbox"
+                        >
                             <Checkbox checked={isSelected} />
                         </TableCell>
-                        {CardsHeadCells.map((headCell) => (
+                        {cardsHeadCells.map((headCell) => (
                             <TableCell
                                 key={headCell.id}
                                 align={headCell.align}
                                 padding={headCell.padding}
                             >
-                                <Typography>{row[headCell.id]}</Typography>
+                                <Typography display="inline">
+                                    {headCell.convert
+                                        ? headCell.convert(row[headCell.id])
+                                        : (row[headCell.id] as string)}
+                                </Typography>
+                                {headCell.id === "name" &&
+                                    editButton(row.id, row[headCell.id])}
                             </TableCell>
                         ))}
                     </TableRow>
@@ -63,6 +125,3 @@ export const CardsTableBody: FC<CardsTableBodyProps> = ({
         </TableBody>
     );
 };
-
-// TODO: Go over all mouse events
-// TODO: Move all components props
diff --git a/vinvoor/src/cards/CardsTableHead.tsx b/vinvoor/src/cards/CardsTableHead.tsx
index c8d5589..9c651c0 100644
--- a/vinvoor/src/cards/CardsTableHead.tsx
+++ b/vinvoor/src/cards/CardsTableHead.tsx
@@ -7,8 +7,8 @@ import {
     Typography,
 } from "@mui/material";
 import { ChangeEvent, FC, MouseEvent } from "react";
-import { Card, CardsHeadCells } from "../types/cards";
-import { TableOrder } from "../types/table";
+import { Card, cardsHeadCells } from "../types/cards";
+import { TableOrder } from "../types/general";
 
 interface CardTableHeadProps {
     numSelected: number;
@@ -46,7 +46,7 @@ export const CardsTableHead: FC<CardTableHeadProps> = ({
                         onChange={onSelectAllClick}
                     />
                 </TableCell>
-                {CardsHeadCells.map((headCell) => (
+                {cardsHeadCells.map((headCell) => (
                     <TableCell
                         key={headCell.id}
                         align={headCell.align}
diff --git a/vinvoor/src/cards/CardsTableToolbar.tsx b/vinvoor/src/cards/CardsTableToolbar.tsx
index 5057be0..f9dc865 100644
--- a/vinvoor/src/cards/CardsTableToolbar.tsx
+++ b/vinvoor/src/cards/CardsTableToolbar.tsx
@@ -1,19 +1,14 @@
 import { Toolbar, Typography } from "@mui/material";
 import { alpha } from "@mui/material/styles";
-import { Dispatch, FC, SetStateAction } from "react";
-import { Card } from "../types/cards";
+import { FC } from "react";
 import { CardsAdd } from "./CardsAdd";
 import { CardsDelete } from "./CardsDelete";
 
 interface CardTableToolbarProps {
     selected: readonly string[];
-    setCards: Dispatch<SetStateAction<readonly Card[]>>;
 }
 
-export const CardsTableToolbar: FC<CardTableToolbarProps> = ({
-    selected,
-    setCards,
-}) => {
+export const CardsTableToolbar: FC<CardTableToolbarProps> = ({ selected }) => {
     const numSelected = selected.length;
 
     return (
@@ -38,7 +33,7 @@ export const CardsTableToolbar: FC<CardTableToolbarProps> = ({
                     >
                         {numSelected} selected
                     </Typography>
-                    <CardsDelete selected={selected} setCards={setCards} />
+                    <CardsDelete selected={selected} />
                 </>
             ) : (
                 <>
diff --git a/vinvoor/src/cards/CircularTimeProgress.tsx b/vinvoor/src/cards/CircularTimeProgress.tsx
new file mode 100644
index 0000000..be2893a
--- /dev/null
+++ b/vinvoor/src/cards/CircularTimeProgress.tsx
@@ -0,0 +1,31 @@
+import { Box, CircularProgress, Typography } from "@mui/material";
+import { FC } from "react";
+
+export interface CircularTimeProgressProps {
+    time: number;
+    percentage: number;
+}
+
+export const CircularTimeProgress: FC<CircularTimeProgressProps> = ({
+    time,
+    percentage,
+}) => {
+    return (
+        <Box display="flex" sx={{ alignItems: "center" }}>
+            <CircularProgress
+                variant="determinate"
+                value={percentage * 100}
+                size={25}
+                sx={{ mr: "5px" }}
+            />
+            <Typography
+                variant="caption"
+                component="div"
+                color="primary"
+                sx={{ position: "absolute", left: 21 }}
+            >
+                {Math.round(time)}
+            </Typography>
+        </Box>
+    );
+};
diff --git a/vinvoor/src/components/ConfirmationModal.tsx b/vinvoor/src/components/ConfirmationModal.tsx
deleted file mode 100644
index 3d83924..0000000
--- a/vinvoor/src/components/ConfirmationModal.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { Box, Modal } from "@mui/material";
-import { FC, ReactNode } from "react";
-import { TypographyG } from "./TypographyG";
-
-interface ConfirmationModalProps {
-    open: boolean;
-    onClose: () => void;
-    title: string;
-    content: ReactNode;
-    actions: ReactNode;
-}
-
-export const ConfirmationModal: FC<ConfirmationModalProps> = ({
-    open,
-    onClose,
-    title,
-    content,
-    actions,
-}) => {
-    return (
-        <Modal open={open} onClose={onClose} keepMounted>
-            <Box
-                sx={{
-                    position: "absolute",
-                    top: "50%",
-                    left: "50%",
-                    transform: "translate(-50%, -50%)",
-                    bgcolor: "background.paper",
-                    border: "2px solid #000",
-                    boxShadow: 24,
-                    pt: 2,
-                    px: 4,
-                    pb: 3,
-                }}
-            >
-                <Box
-                    sx={{
-                        display: "flex",
-                        flexDirection: "column",
-                        justifyContent: "center",
-                        alignItems: "center",
-                        textAlign: "center",
-                        padding: 2,
-                    }}
-                >
-                    <TypographyG variant="h4">{title}</TypographyG>
-                    <TypographyG variant="body1">{content}</TypographyG>
-                </Box>
-                <Box
-                    sx={{
-                        display: "flex",
-                        justifyContent: "space-between",
-                        mt: 6,
-                    }}
-                >
-                    {actions}
-                </Box>
-            </Box>
-        </Modal>
-    );
-};
diff --git a/vinvoor/src/components/UnstyledLink.tsx b/vinvoor/src/components/UnstyledLink.tsx
index 5de2736..d1c160c 100644
--- a/vinvoor/src/components/UnstyledLink.tsx
+++ b/vinvoor/src/components/UnstyledLink.tsx
@@ -2,5 +2,7 @@ import { FC } from "react";
 import { Link, LinkProps } from "react-router-dom";
 
 export const UnstyledLink: FC<LinkProps> = (props) => {
-    return <Link {...props} style={{ textDecoration: "none" }} />;
+    return (
+        <Link {...props} style={{ color: "inherit", textDecoration: "none" }} />
+    );
 };
diff --git a/vinvoor/src/hooks/useFetch.ts b/vinvoor/src/hooks/useFetch.ts
index 0a2b1ac..c26ac0b 100644
--- a/vinvoor/src/hooks/useFetch.ts
+++ b/vinvoor/src/hooks/useFetch.ts
@@ -1,5 +1,5 @@
 import { Dispatch, SetStateAction, useEffect, useState } from "react";
-import { fetchApi } from "../util/fetch";
+import { getApi } from "../util/fetch";
 
 interface useFetchResult {
     loading: boolean;
@@ -8,13 +8,14 @@ interface useFetchResult {
 
 export const useFetch = <T>(
     endpoint: string,
-    setData: Dispatch<SetStateAction<T>>
+    setData: Dispatch<SetStateAction<T>>,
+    convertData?: (data: any) => T
 ): useFetchResult => {
     const [loading, setLoading] = useState<boolean>(true);
     const [error, setError] = useState<Error | undefined>(undefined);
 
     useEffect(() => {
-        fetchApi(endpoint)
+        getApi<T>(endpoint, convertData)
             .then((data) => setData(data))
             .catch((error) => setError(error))
             .finally(() => setLoading(false));
diff --git a/vinvoor/src/leaderboard/Leaderboard.tsx b/vinvoor/src/leaderboard/Leaderboard.tsx
index eed034c..ca9ebb6 100644
--- a/vinvoor/src/leaderboard/Leaderboard.tsx
+++ b/vinvoor/src/leaderboard/Leaderboard.tsx
@@ -10,7 +10,7 @@ export const Leaderboard = () => {
     const [leaderboardItems, setLeaderboardItems] = useState<
         readonly LeaderboardItem[]
     >([]);
-    const { loading, error: _ } = useFetch<readonly LeaderboardItem[]>(
+    const { loading } = useFetch<readonly LeaderboardItem[]>(
         "leaderboard",
         setLeaderboardItems
     );
@@ -24,7 +24,6 @@ export const Leaderboard = () => {
                 />
                 <TableContainer>
                     <Table>
-                        {/* <LeaderboardTableHead /> */}
                         <LeaderboardTableBody
                             leaderboardItems={leaderboardItems}
                         />
diff --git a/vinvoor/src/leaderboard/LeaderboardTableBody.tsx b/vinvoor/src/leaderboard/LeaderboardTableBody.tsx
index 81a286d..7f94e90 100644
--- a/vinvoor/src/leaderboard/LeaderboardTableBody.tsx
+++ b/vinvoor/src/leaderboard/LeaderboardTableBody.tsx
@@ -1,74 +1,125 @@
 import {
-    styled,
+    Icon,
     TableBody,
     TableCell,
-    tableCellClasses,
     TableRow,
     Typography,
 } from "@mui/material";
-import { PodiumBronze, PodiumGold, PodiumSilver } from "mdi-material-ui";
-import { FC } from "react";
+import { alpha } from "@mui/material/styles";
+import {
+    ArrowDownBoldHexagonOutline,
+    ArrowUpBoldHexagonOutline,
+    Minus,
+} from "mdi-material-ui";
+import { FC, useContext } from "react";
+import { TableHeadCell } from "../types/general";
 import { leaderboardHeadCells, LeaderboardItem } from "../types/leaderboard";
+import { UserContext } from "../user/UserProvider";
+import FirstPlaceIcon from "/first_place.svg";
+import SecondPlaceIcon from "/second_place.svg";
+import ThirdPlaceIcon from "/third_place.svg";
 
 interface LeaderboardTableBodyProps {
     leaderboardItems: readonly LeaderboardItem[];
 }
 
-const StyledTableCell = styled(TableCell)(({ theme }) => ({
-    [`&.${tableCellClasses.head}`]: {
-        backgroundColor: theme.palette.common.black,
-        color: theme.palette.common.white,
-    },
-    [`&.${tableCellClasses.body}`]: {
-        fontSize: 14,
-    },
-}));
+const getPositionChange = (positionChange: number) => {
+    let icon: JSX.Element | null = null;
+
+    if (positionChange > 0) {
+        icon = <ArrowUpBoldHexagonOutline color="success" />;
+    } else if (positionChange < 0) {
+        icon = <ArrowDownBoldHexagonOutline color="error" />;
+    } else {
+        icon = <Minus />;
+    }
 
-const StyledTableRow = styled(TableRow)(({ theme }) => ({
-    "&:nth-of-type(odd)": {
-        backgroundColor: theme.palette.action.hover,
-    },
-    // hide last border
-    "&:last-child td, &:last-child th": {
-        border: 0,
-    },
-}));
+    return (
+        <>
+            {icon}
+            <Typography>{positionChange}</Typography>
+        </>
+    );
+};
 
 const getPosition = (position: number) => {
     switch (position) {
         case 1:
-            return <PodiumGold htmlColor="#FFD700" />;
+            // return <PodiumGold htmlColor="#FFD700" />;
+            return (
+                <Icon>
+                    <img src={FirstPlaceIcon} />
+                </Icon>
+            );
         case 2:
-            return <PodiumSilver htmlColor="#C0C0C0" />;
+            return (
+                <Icon>
+                    <img src={SecondPlaceIcon} />
+                </Icon>
+            );
         case 3:
-            return <PodiumBronze htmlColor="#CD7F32" />;
+            return (
+                <Icon>
+                    <img src={ThirdPlaceIcon} />
+                </Icon>
+            );
         default:
             return <Typography fontWeight="bold">{position}</Typography>;
     }
 };
 
+const getCell = (
+    row: LeaderboardItem,
+    headCell: TableHeadCell<LeaderboardItem>
+) => {
+    switch (headCell.id) {
+        case "positionChange":
+            return getPositionChange(row[headCell.id]);
+        case "position":
+            return getPosition(row[headCell.id]);
+        default:
+            return <Typography>{row[headCell.id]}</Typography>;
+    }
+};
+
 export const LeaderboardTableBody: FC<LeaderboardTableBodyProps> = ({
     leaderboardItems: rows,
 }) => {
+    const {
+        userState: { user },
+    } = useContext(UserContext);
+
     return (
         <TableBody>
-            {rows.map((row) => {
+            {rows.map((row, index) => {
                 return (
-                    <StyledTableRow key={row.username} id={row.username}>
+                    <TableRow
+                        key={row.username}
+                        id={row.username}
+                        sx={{
+                            ...(index % 2 === 0 && {
+                                backgroundColor: (theme) =>
+                                    theme.palette.action.hover,
+                            }),
+                            ...(row.username === user!.username && {
+                                backgroundColor: (theme) =>
+                                    alpha(
+                                        theme.palette.primary.main,
+                                        theme.palette.action.activatedOpacity
+                                    ),
+                            }),
+                        }}
+                    >
                         {leaderboardHeadCells.map((headCell) => (
-                            <StyledTableCell
+                            <TableCell
                                 key={headCell.id}
                                 align={headCell.align}
                                 padding={headCell.padding}
                             >
-                                {headCell.id === "position" ? (
-                                    getPosition(row[headCell.id])
-                                ) : (
-                                    <Typography>{row[headCell.id]}</Typography>
-                                )}
-                            </StyledTableCell>
+                                {getCell(row, headCell)}
+                            </TableCell>
                         ))}
-                    </StyledTableRow>
+                    </TableRow>
                 );
             })}
         </TableBody>
diff --git a/vinvoor/src/main.tsx b/vinvoor/src/main.tsx
index 8f070ec..f45b059 100644
--- a/vinvoor/src/main.tsx
+++ b/vinvoor/src/main.tsx
@@ -3,13 +3,17 @@ import "@fontsource/roboto/400.css";
 import "@fontsource/roboto/500.css";
 import "@fontsource/roboto/700.css";
 import { CssBaseline } from "@mui/material";
+import { ConfirmProvider } from "material-ui-confirm";
+import { SnackbarProvider } from "notistack";
 import React from "react";
 import ReactDOM from "react-dom/client";
 import { createBrowserRouter, RouterProvider } from "react-router-dom";
+import "react-tooltip/dist/react-tooltip.css";
 import { App } from "./App.tsx";
 import { Cards } from "./cards/Cards.tsx";
 import { ErrorPage } from "./errors/ErrorPage.tsx";
 import { Leaderboard } from "./leaderboard/Leaderboard.tsx";
+import { Scans } from "./scans/Scans.tsx";
 import { ThemeProvider } from "./theme/ThemeProvider";
 import { Login } from "./user/Login.tsx";
 import { Logout } from "./user/Logout.tsx";
@@ -21,10 +25,18 @@ const router = createBrowserRouter([
         element: <App />,
         errorElement: <ErrorPage />,
         children: [
+            {
+                path: "login",
+                element: <Login />,
+            },
             {
                 path: "logout",
                 element: <Logout />,
             },
+            {
+                path: "scans",
+                element: <Scans />,
+            },
             {
                 path: "cards",
                 element: <Cards />,
@@ -33,13 +45,12 @@ const router = createBrowserRouter([
                 path: "leaderboard",
                 element: <Leaderboard />,
             },
+            {
+                path: "settings",
+                
+            }
         ],
     },
-    {
-        path: "/login",
-        element: <Login />,
-        errorElement: <ErrorPage />,
-    },
 ]);
 
 ReactDOM.createRoot(document.getElementById("root")!).render(
@@ -47,7 +58,16 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
         <ThemeProvider>
             <CssBaseline enableColorScheme>
                 <UserProvider>
-                    <RouterProvider router={router} />
+                    <ConfirmProvider>
+                        <SnackbarProvider
+                            anchorOrigin={{
+                                horizontal: "center",
+                                vertical: "top",
+                            }}
+                        >
+                            <RouterProvider router={router} />
+                        </SnackbarProvider>
+                    </ConfirmProvider>
                 </UserProvider>
             </CssBaseline>
         </ThemeProvider>
diff --git a/vinvoor/src/navbar/NavBar.tsx b/vinvoor/src/navbar/NavBar.tsx
index be65857..a07435d 100644
--- a/vinvoor/src/navbar/NavBar.tsx
+++ b/vinvoor/src/navbar/NavBar.tsx
@@ -1,4 +1,9 @@
+import LeaderboardIcon from "@mui/icons-material/Leaderboard";
 import { AppBar, Box, Container, Toolbar } from "@mui/material";
+import {
+    CreditCardMultipleOutline,
+    CreditCardScanOutline,
+} from "mdi-material-ui";
 import { useContext } from "react";
 import { DarkModeToggle } from "../components/DarkModeToggle";
 import { UserContext } from "../user/UserProvider";
@@ -7,8 +12,18 @@ import { NavBarPages } from "./NavBarPages";
 import { NavBarSandwich } from "./NavBarSandwich";
 import { NavBarUserMenu } from "./NavBarUserMenu";
 
-const pages = ["Cards", "Leaderboard"];
-const settings = ["Logout"];
+export interface PageIcon {
+    page: string;
+    icon: JSX.Element;
+}
+
+const navBarPages: PageIcon[] = [
+    { page: "Scans", icon: <CreditCardScanOutline sx={{ mr: ".3rem" }} /> },
+    { page: "Cards", icon: <CreditCardMultipleOutline sx={{ mr: ".3rem" }} /> },
+    { page: "Leaderboard", icon: <LeaderboardIcon sx={{ mr: ".3rem" }} /> },
+];
+
+const userMenuPages: PageIcon[] = [];
 
 export const NavBar = () => {
     const {
@@ -21,7 +36,13 @@ export const NavBar = () => {
     };
 
     return (
-        <AppBar position="static">
+        <AppBar
+            position="static"
+            sx={{
+                background:
+                    "rgb(255,164,0) linear-gradient(45deg, rgba(255,164,0,1) 0%, rgba(255,127,0,1) 100%)",
+            }}
+        >
             <Container maxWidth="xl">
                 <Toolbar disableGutters>
                     {/* Display either the ZeSS logo or a sandwich menu */}
@@ -31,7 +52,7 @@ export const NavBar = () => {
 
                         {user && (
                             <NavBarSandwich
-                                pages={pages}
+                                pageIcons={navBarPages}
                                 sx={{ display: screenSize.mobile }}
                             />
                         )}
@@ -42,7 +63,7 @@ export const NavBar = () => {
                     <Box sx={{ flexGrow: 1 }}>
                         {user && (
                             <NavBarPages
-                                pages={pages}
+                                pageIcons={navBarPages}
                                 sx={{ display: screenSize.desktop }}
                             />
                         )}
@@ -54,7 +75,7 @@ export const NavBar = () => {
 
                     <Box sx={{ flexGrow: 0 }}>
                         <DarkModeToggle />
-                        <NavBarUserMenu settings={settings} />
+                        <NavBarUserMenu pageIcons={userMenuPages} />
                     </Box>
                 </Toolbar>
             </Container>
diff --git a/vinvoor/src/navbar/NavBarLogo.tsx b/vinvoor/src/navbar/NavBarLogo.tsx
index 58c6e2a..015a46d 100644
--- a/vinvoor/src/navbar/NavBarLogo.tsx
+++ b/vinvoor/src/navbar/NavBarLogo.tsx
@@ -1,4 +1,5 @@
-import { Button, SxProps, Theme, Typography } from "@mui/material";
+import { Box, Button, SxProps, Theme, Typography } from "@mui/material";
+import { HexagonSlice6 } from "mdi-material-ui";
 import { FC } from "react";
 import { UnstyledLink } from "../components/UnstyledLink";
 
@@ -8,25 +9,29 @@ interface NavBarLogoProps {
 
 export const NavBarLogo: FC<NavBarLogoProps> = ({ sx }) => {
     return (
-        <UnstyledLink to="/">
-            <Button
-                color="inherit"
-                sx={{
-                    ...sx,
-                    textTransform: "none",
-                    color: "white",
-                }}
-            >
-                <Typography
-                    variant="h6"
+        <Box display="flex">
+            <UnstyledLink to="/">
+                <Button
+                    color="inherit"
                     sx={{
-                        letterSpacing: ".3rem",
-                        fontWeight: 700,
+                        ...sx,
+                        textTransform: "none",
+                        color: "white",
                     }}
                 >
-                    ZeSS
-                </Typography>
-            </Button>
-        </UnstyledLink>
+                    <HexagonSlice6 sx={{ mr: ".3rem" }} />
+                    <Typography
+                        variant="h6"
+                        sx={{
+                            letterSpacing: ".3rem",
+                            fontWeight: 700,
+                        }}
+                    >
+                        ZeSS
+                    </Typography>
+                </Button>
+            </UnstyledLink>
+            <Box sx={{ flexGrow: 1 }} />
+        </Box>
     );
 };
diff --git a/vinvoor/src/navbar/NavBarPages.tsx b/vinvoor/src/navbar/NavBarPages.tsx
index 7e086f3..d367fdd 100644
--- a/vinvoor/src/navbar/NavBarPages.tsx
+++ b/vinvoor/src/navbar/NavBarPages.tsx
@@ -1,23 +1,25 @@
 import { Box, Button, SxProps, Theme, Typography } from "@mui/material";
 import { FC } from "react";
 import { UnstyledLink } from "../components/UnstyledLink";
+import { PageIcon } from "./NavBar";
 
 interface NavBarPagesProps {
-    pages: readonly string[];
+    pageIcons: readonly PageIcon[];
     sx?: SxProps<Theme>;
 }
 
-export const NavBarPages: FC<NavBarPagesProps> = ({ pages, sx }) => {
+export const NavBarPages: FC<NavBarPagesProps> = ({ pageIcons, sx }) => {
     return (
         <Box sx={{ ...sx }}>
-            {pages.map((page) => (
+            {pageIcons.map(({ page, icon }) => (
                 <UnstyledLink key={page} to={page.toLowerCase()}>
                     <Button
-                        color="inherit"
                         sx={{
                             color: "white",
                         }}
                     >
+                        {icon}
+
                         <Typography>{page}</Typography>
                     </Button>
                 </UnstyledLink>
diff --git a/vinvoor/src/navbar/NavBarSandwich.tsx b/vinvoor/src/navbar/NavBarSandwich.tsx
index 9c46319..71c63e2 100644
--- a/vinvoor/src/navbar/NavBarSandwich.tsx
+++ b/vinvoor/src/navbar/NavBarSandwich.tsx
@@ -10,13 +10,14 @@ import {
 } from "@mui/material";
 import { FC, MouseEvent, useState } from "react";
 import { UnstyledLink } from "../components/UnstyledLink";
+import { PageIcon } from "./NavBar";
 
 interface NavBarSandwichProps {
-    pages: readonly string[];
+    pageIcons: readonly PageIcon[];
     sx?: SxProps<Theme>;
 }
 
-export const NavBarSandwich: FC<NavBarSandwichProps> = ({ pages, sx }) => {
+export const NavBarSandwich: FC<NavBarSandwichProps> = ({ pageIcons, sx }) => {
     const [anchorElNav, setAnchorElNav] = useState<undefined | HTMLElement>(
         undefined
     );
@@ -48,9 +49,10 @@ export const NavBarSandwich: FC<NavBarSandwichProps> = ({ pages, sx }) => {
                 open={Boolean(anchorElNav)}
                 onClose={handleCloseNavMenu}
             >
-                {pages.map((page) => (
+                {pageIcons.map(({ page, icon }) => (
                     <UnstyledLink key={page} to={page.toLowerCase()}>
                         <MenuItem onClick={handleCloseNavMenu}>
+                            {icon}
                             <Typography>{page}</Typography>
                         </MenuItem>
                     </UnstyledLink>
diff --git a/vinvoor/src/navbar/NavBarUserMenu.tsx b/vinvoor/src/navbar/NavBarUserMenu.tsx
index 8d64df7..dc77d6e 100644
--- a/vinvoor/src/navbar/NavBarUserMenu.tsx
+++ b/vinvoor/src/navbar/NavBarUserMenu.tsx
@@ -1,14 +1,18 @@
 import { AccountCircle } from "@mui/icons-material";
 import { Button, Menu, MenuItem, Typography } from "@mui/material";
+import ExitRun from "mdi-material-ui/ExitRun";
 import { FC, MouseEvent, useContext, useState } from "react";
 import { UnstyledLink } from "../components/UnstyledLink";
+import { Login } from "../user/Login";
+import { Logout } from "../user/Logout";
 import { UserContext } from "../user/UserProvider";
+import { PageIcon } from "./NavBar";
 
 interface NavBarUserMenuProps {
-    settings: readonly string[];
+    pageIcons: readonly PageIcon[];
 }
 
-export const NavBarUserMenu: FC<NavBarUserMenuProps> = ({ settings }) => {
+export const NavBarUserMenu: FC<NavBarUserMenuProps> = ({ pageIcons }) => {
     const {
         userState: { user },
     } = useContext(UserContext);
@@ -29,7 +33,6 @@ export const NavBarUserMenu: FC<NavBarUserMenuProps> = ({ settings }) => {
             {user ? (
                 <>
                     <Button
-                        color="inherit"
                         onClick={handleOpenUserMenu}
                         sx={{
                             textTransform: "none",
@@ -37,9 +40,7 @@ export const NavBarUserMenu: FC<NavBarUserMenuProps> = ({ settings }) => {
                         }}
                     >
                         <AccountCircle sx={{ mr: "3px" }} />
-                        <Typography variant="h6" sx={{ color: "white" }}>
-                            {user.username}
-                        </Typography>
+                        <Typography variant="h6">{user.username}</Typography>
                     </Button>
                     <Menu
                         sx={{ mt: "45px" }}
@@ -56,31 +57,26 @@ export const NavBarUserMenu: FC<NavBarUserMenuProps> = ({ settings }) => {
                         open={Boolean(anchorElUser)}
                         onClose={handleCloseUserMenu}
                     >
-                        {settings.map((setting) => (
-                            <UnstyledLink
-                                key={setting}
-                                to={setting.toLowerCase()}
-                            >
+                        {pageIcons.map(({ page, icon }) => (
+                            <UnstyledLink key={page} to={page.toLowerCase()}>
                                 <MenuItem onClick={handleCloseUserMenu}>
-                                    <Typography>{setting}</Typography>
+                                    {icon}
+                                    <Typography>{page}</Typography>
                                 </MenuItem>
                             </UnstyledLink>
                         ))}
+                        <MenuItem>
+                            <Logout sx={{ color: "inherit" }}>
+                                <ExitRun sx={{ mr: ".3rem" }} />
+                                <Typography>Logout</Typography>
+                            </Logout>
+                        </MenuItem>
                     </Menu>
                 </>
             ) : (
-                <UnstyledLink to="login">
-                    <Button
-                        color="inherit"
-                        size="large"
-                        sx={{
-                            textTransform: "none",
-                            color: "white",
-                        }}
-                    >
-                        <Typography>Login</Typography>
-                    </Button>
-                </UnstyledLink>
+                <Login sx={{ color: "inherit", size: "large" }}>
+                    <Typography>Login</Typography>
+                </Login>
             )}
         </>
     );
diff --git a/vinvoor/src/overview/Overview.tsx b/vinvoor/src/overview/Overview.tsx
new file mode 100644
index 0000000..4060dad
--- /dev/null
+++ b/vinvoor/src/overview/Overview.tsx
@@ -0,0 +1,127 @@
+import { Box, Paper, Switch, Typography } from "@mui/material";
+import Grid from "@mui/material/Grid";
+import { createContext, useEffect, useRef, useState } from "react";
+import { BrowserView } from "react-device-detect";
+import { Tooltip } from "react-tooltip";
+import { LoadingSkeleton } from "../components/LoadingSkeleton";
+import { useFetch } from "../hooks/useFetch";
+import { convertScanJSON, Scan } from "../types/scans";
+import { CheckIn } from "./checkin/CheckIn";
+import { Days } from "./days/Days";
+import { Heatmap, HeatmapVariant } from "./heatmap/Heatmap";
+import { Streak } from "./streak/Streak";
+
+interface ScanContextProps {
+    scans: readonly Scan[];
+}
+
+export const ScanContext = createContext<ScanContextProps>({
+    scans: [],
+});
+
+export const Overview = () => {
+    const [scans, setScans] = useState<readonly Scan[]>([]);
+    const { loading } = useFetch<readonly Scan[]>(
+        "scans",
+        setScans,
+        convertScanJSON
+    );
+    const [checked, setChecked] = useState<boolean>(false);
+    const daysRef = useRef<HTMLDivElement>(null);
+    const heatmapSwitchRef = useRef<HTMLDivElement>(null);
+    const [heatmapSwitchHeight, setHeatmapSwitchHeight] = useState<number>(0);
+    const [paperHeight, setPaperHeight] = useState<number>(0);
+
+    const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+        setChecked(event.target.checked);
+    };
+
+    useEffect(() => {
+        if (daysRef.current) {
+            setPaperHeight(daysRef.current.clientHeight);
+        }
+
+        if (heatmapSwitchRef.current) {
+            setHeatmapSwitchHeight(heatmapSwitchRef.current.clientHeight);
+        }
+    });
+
+    return (
+        <LoadingSkeleton loading={loading}>
+            <ScanContext.Provider value={{ scans }}>
+                <Grid
+                    container
+                    spacing={2}
+                    alignItems="stretch"
+                    justifyContent="space-between"
+                >
+                    <Grid item xs={8} md={4} lg={3}>
+                        <CheckIn />
+                    </Grid>
+                    <Grid item xs={4}>
+                        <Streak />
+                    </Grid>
+                    <Grid
+                        item
+                        xs={12}
+                        md={8}
+                        sx={{
+                            display: "flex",
+                        }}
+                    >
+                        <Paper
+                            elevation={4}
+                            sx={{
+                                padding: 1,
+                                width: "100%",
+                                height: paperHeight,
+                            }}
+                        >
+                            <BrowserView>
+                                <Box
+                                    sx={{
+                                        display: "flex",
+                                        justifyContent: "right",
+                                    }}
+                                    ref={heatmapSwitchRef}
+                                >
+                                    <Typography variant="h6">Months</Typography>
+                                    <Switch
+                                        checked={checked}
+                                        onChange={handleChange}
+                                    />
+                                    <Typography variant="h6">Days</Typography>
+                                </Box>
+                            </BrowserView>
+                            <Heatmap
+                                startDate={new Date("2024-04-01")}
+                                endDate={new Date("2024-12-31")}
+                                variant={
+                                    checked
+                                        ? HeatmapVariant.DAYS
+                                        : HeatmapVariant.MONTHS
+                                }
+                                maxHeight={
+                                    paperHeight - heatmapSwitchHeight - 10
+                                }
+                            />
+                            <Tooltip id="heatmap" />
+                        </Paper>
+                    </Grid>
+                    <Grid item xs={12} md={4} sx={{ display: "flex" }}>
+                        <Paper
+                            elevation={4}
+                            sx={{ padding: 2, width: "100%" }}
+                            ref={daysRef}
+                        >
+                            <Days />
+                        </Paper>
+                    </Grid>
+                </Grid>
+            </ScanContext.Provider>
+        </LoadingSkeleton>
+    );
+};
+
+// Current height of the heatmap is calculated using ref's and calculus
+// TODO: Change it as it is very very very very very very ugly ^^
diff --git a/vinvoor/src/overview/checkin/CheckIn.tsx b/vinvoor/src/overview/checkin/CheckIn.tsx
new file mode 100644
index 0000000..d174615
--- /dev/null
+++ b/vinvoor/src/overview/checkin/CheckIn.tsx
@@ -0,0 +1,37 @@
+import { Alert, AlertTitle } from "@mui/material";
+import { EmoticonExcitedOutline, EmoticonFrownOutline } from "mdi-material-ui";
+import { useContext } from "react";
+import { isTheSameDay } from "../../util/util";
+import { ScanContext } from "../Overview";
+
+export const CheckIn = () => {
+    const { scans } = useContext(ScanContext);
+
+    const checkedIn =
+        scans.length > 0 &&
+        isTheSameDay(scans[scans.length - 1].scanTime, new Date());
+
+    return checkedIn ? (
+        <Alert
+            variant="outlined"
+            severity="success"
+            iconMapping={{
+                success: <EmoticonExcitedOutline fontSize="large" />,
+            }}
+        >
+            <AlertTitle>Checked in</AlertTitle>
+            Nice of you to stop by!
+        </Alert>
+    ) : (
+        <Alert
+            variant="outlined"
+            severity="error"
+            iconMapping={{
+                error: <EmoticonFrownOutline fontSize="large" />,
+            }}
+        >
+            <AlertTitle>Not checked in</AlertTitle>
+            We miss you!
+        </Alert>
+    );
+};
diff --git a/vinvoor/src/overview/days/Days.tsx b/vinvoor/src/overview/days/Days.tsx
new file mode 100644
index 0000000..d0f1dab
--- /dev/null
+++ b/vinvoor/src/overview/days/Days.tsx
@@ -0,0 +1,58 @@
+import { ApexOptions } from "apexcharts";
+import { useContext } from "react";
+import Chart from "react-apexcharts";
+import { Scan } from "../../types/scans";
+import { ScanContext } from "../Overview";
+
+const getDayCount = (scans: readonly Scan[]) => {
+    const days = [0, 0, 0, 0, 0, 0, 0];
+    scans.forEach((scan) => {
+        days[scan.scanTime.getDay() - 1]++;
+    });
+    return days.slice(0, -2);
+};
+
+export const Days = () => {
+    const { scans } = useContext(ScanContext);
+
+    const state = {
+        options: {
+            labels: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
+            fill: {
+                opacity: 1,
+            },
+            yaxis: {
+                show: false,
+            },
+            legend: {
+                position: "bottom",
+                labels: {
+                    useSeriesColors: true,
+                },
+            },
+            plotOptions: {
+                polarArea: {
+                    rings: {
+                        strokeWidth: 0,
+                    },
+                    spokes: {
+                        strokeWidth: 0,
+                    },
+                },
+            },
+            theme: {
+                monochrome: {
+                    enabled: true,
+                    color: "#ff7f00",
+                    shadeTo: "light",
+                    shadeIntensity: 1,
+                },
+            },
+        } as ApexOptions,
+        series: getDayCount(scans),
+    };
+
+    return (
+        <Chart options={state.options} series={state.series} type="polarArea" />
+    );
+};
diff --git a/vinvoor/src/overview/heatmap/Heatmap.tsx b/vinvoor/src/overview/heatmap/Heatmap.tsx
new file mode 100644
index 0000000..0189a5f
--- /dev/null
+++ b/vinvoor/src/overview/heatmap/Heatmap.tsx
@@ -0,0 +1,257 @@
+import { Box } from "@mui/material";
+import { FC, useContext } from "react";
+import { MILLISECONDS_IN_ONE_DAY, shiftDate } from "../../util/util";
+import { ScanContext } from "../Overview";
+import "./heatmap.css";
+import {
+    dateTimeFormat,
+    DAYS_IN_WEEK,
+    getColumnCount,
+    getEmpty,
+    getHeight,
+    getMonthLabelCoordinates,
+    getSquareCoordinates,
+    getTransformForAllWeeks,
+    getTransformForColumn,
+    getTransformForMonthLabels,
+    getWidth,
+    MONTH_LABELS,
+    SQUARE_SIZE,
+} from "./utils";
+
+export interface HeatmapItem {
+    date: Date;
+    count: number;
+}
+
+export enum HeatmapVariant {
+    DAYS,
+    MONTHS,
+}
+
+interface HeatmapProps {
+    startDate: Date;
+    endDate: Date;
+    variant: HeatmapVariant;
+    maxHeight: number;
+}
+
+const getAllValues = (
+    days: readonly Date[],
+    startDate: Date,
+    endDate: Date,
+    variant: HeatmapVariant
+): HeatmapItem[] => {
+    const values: readonly HeatmapItem[] = days.map((date) => ({
+        date,
+        count: 1,
+    }));
+    if (variant === HeatmapVariant.DAYS) {
+        return Array.from(
+            {
+                length:
+                    (endDate.getTime() - startDate.getTime()) /
+                        MILLISECONDS_IN_ONE_DAY +
+                    1,
+            },
+            (_, i) => {
+                const date = shiftDate(startDate, i);
+                return (
+                    values.find((v) => v.date.getTime() === date.getTime()) || {
+                        date,
+                        count: 0,
+                    }
+                );
+            }
+        );
+    } else {
+        return Array.from(
+            {
+                length: getColumnCount(startDate, endDate, HeatmapVariant.DAYS),
+            },
+            (_, i) => {
+                const start = shiftDate(startDate, i * DAYS_IN_WEEK);
+                const count = Array.from({
+                    length: DAYS_IN_WEEK,
+                }).reduce<number>((sum, _, j) => {
+                    const date = shiftDate(start, j);
+                    const value = values.find(
+                        (v) => v.date.getTime() === date.getTime()
+                    );
+                    return sum + (value ? value.count : 0);
+                }, 0);
+
+                return { date: start, count };
+            }
+        );
+    }
+};
+
+const getWeeksInMonth = (
+    values: HeatmapItem[],
+    startDate: Date
+): { [key: number]: number } => {
+    const startYear = values[0].date.getFullYear();
+    return values.reduce(
+        (acc, value) => {
+            const index =
+                (value.date.getFullYear() - startYear) * 12 +
+                value.date.getMonth() -
+                startDate.getMonth();
+            acc[index] = (acc[index] || 0) + 1;
+            return acc;
+        },
+        {
+            [startDate.getMonth()]: getEmpty(
+                values[0].date,
+                HeatmapVariant.MONTHS
+            ),
+        } as {
+            [key: number]: number;
+        }
+    );
+};
+
+const getClassNameForValue = (value: HeatmapItem, variant: HeatmapVariant) => {
+    if (variant === HeatmapVariant.DAYS) {
+        if (value.count > 0) return `color-active`;
+
+        return `color-inactive`;
+    } else {
+        if (value.count <= 5) return `color-${value.count}`;
+
+        return "color-5";
+    }
+};
+
+const getTooltipDataAttrsForDate = (
+    value: HeatmapItem,
+    variant: HeatmapVariant
+) => ({
+    "data-tooltip-id": "heatmap",
+    "data-tooltip-content":
+        variant === HeatmapVariant.DAYS
+            ? getTooltipDataAttrsForDays(value)
+            : getTooltipDataAttrsForMonths(value),
+});
+
+const getTooltipDataAttrsForDays = (value: HeatmapItem) =>
+    `${value.count > 0 ? "Present" : "Absent"} on ${dateTimeFormat.format(
+        value.date
+    )}`;
+
+const getTooltipDataAttrsForMonths = (value: HeatmapItem) =>
+    `${value.count} scan${
+        value.count !== 1 ? "s" : ""
+    } on the week of ${dateTimeFormat.format(value.date)}`;
+
+export const Heatmap: FC<HeatmapProps> = ({
+    startDate,
+    endDate,
+    variant,
+    maxHeight,
+}) => {
+    const { scans } = useContext(ScanContext);
+    const days = scans.map((scan) => scan.scanTime);
+
+    days.forEach((date) => date.setHours(0, 0, 0, 0));
+    startDate.setHours(0, 0, 0, 0);
+    endDate.setHours(0, 0, 0, 0);
+
+    const values = getAllValues(days, startDate, endDate, variant);
+
+    const viewBox = `0 0 ${getWidth(startDate, endDate, variant)} ${getHeight(
+        variant
+    )}`;
+
+    const weeksInMonth =
+        variant === HeatmapVariant.MONTHS
+            ? getWeeksInMonth(values, startDate)
+            : {}; // Amount of weeks in each month
+
+    const columns = getColumnCount(startDate, endDate, variant); // Amount of columns of squares
+    const emptyStart = getEmpty(startDate, variant); // Amount of empty squares at the start
+    const emptyEnd = getEmpty(endDate, variant); // Amount of empty squares at the end
+
+    let valueIndex = 0;
+    const renderSquare = (row: number, column: number) => {
+        if (column === 0 && row < emptyStart) return null;
+
+        if (variant === HeatmapVariant.DAYS)
+            if (column === columns - 1 && row > emptyEnd) return null;
+
+        const value = values[valueIndex++];
+
+        const [x, y] = getSquareCoordinates(row);
+
+        return (
+            <rect
+                key={row}
+                width={SQUARE_SIZE}
+                height={SQUARE_SIZE}
+                x={x}
+                y={y}
+                rx={2}
+                ry={2}
+                className={`rect ${getClassNameForValue(value, variant)}`}
+                {...getTooltipDataAttrsForDate(value, variant)}
+            />
+        );
+    };
+
+    const renderColumn = (column: number) => (
+        <g key={column} transform={getTransformForColumn(column)}>
+            {[
+                ...Array(
+                    variant === HeatmapVariant.DAYS
+                        ? DAYS_IN_WEEK
+                        : weeksInMonth[column]
+                ).keys(),
+            ].map((row) => renderSquare(row, column))}
+        </g>
+    );
+
+    const renderColumns = () =>
+        [...Array(columns).keys()].map((column) => renderColumn(column));
+
+    const renderMonthLabels = () => {
+        if (variant === HeatmapVariant.DAYS) {
+            return [...Array(columns).keys()].map((column) => {
+                const endOfWeek = shiftDate(startDate, column * DAYS_IN_WEEK);
+                const [x, y] = getMonthLabelCoordinates(column);
+
+                return endOfWeek.getDate() >= 1 &&
+                    endOfWeek.getDate() <= DAYS_IN_WEEK ? (
+                    <text key={column} x={x} y={y}>
+                        {MONTH_LABELS[endOfWeek.getMonth()]}
+                    </text>
+                ) : null;
+            });
+        } else {
+            return [...Array(columns).keys()].map((column) => {
+                if (column % 2 === 1) {
+                    return null;
+                }
+
+                const [x, y] = getMonthLabelCoordinates(column);
+
+                return (
+                    <text key={column} x={x} y={y}>
+                        {MONTH_LABELS[startDate.getMonth() + column]}
+                    </text>
+                );
+            });
+        }
+    };
+
+    return (
+        <Box maxHeight={maxHeight} sx={{ display: "flex" }}>
+            <svg className="heatmap" viewBox={viewBox}>
+                <g transform={getTransformForMonthLabels()}>
+                    {renderMonthLabels()}
+                </g>
+                <g transform={getTransformForAllWeeks()}>{renderColumns()}</g>
+            </svg>
+        </Box>
+    );
+};
diff --git a/vinvoor/src/overview/heatmap/heatmap.css b/vinvoor/src/overview/heatmap/heatmap.css
new file mode 100644
index 0000000..231fe10
--- /dev/null
+++ b/vinvoor/src/overview/heatmap/heatmap.css
@@ -0,0 +1,76 @@
+.heatmap text {
+    font-size: 10px;
+    fill: #aaa;
+}
+
+.heatmap rect:hover {
+    stroke-width: 1px;
+    stroke-opacity: 0;
+}
+
+/*
+    Gradients for months variant
+*/
+
+.heatmap .color-0 {
+    fill: #eeeeee;
+    stroke-width: 1px;
+    stroke: #fcce9f;
+}
+.heatmap .color-1 {
+    fill: #fcce9f;
+    stroke-width: 1px;
+    stroke: #fcbb79;
+}
+
+.heatmap .color-2 {
+    fill: #fcbb79;
+    stroke-width: 1px;
+    stroke: #fa922a;
+}
+.heatmap .color-3 {
+    fill: #fa922a;
+    stroke-width: 1px;
+    stroke: #ff7f00;
+}
+
+.heatmap .color-4 {
+    fill: #ff7f00;
+    stroke-width: 1px;
+    stroke: #ba5f02;
+}
+.heatmap .color-5 {
+    fill: #ba5f02;
+    stroke-width: 1px;
+    stroke: #934b01;
+}
+
+/*
+    Active or not active for days variant
+*/
+
+.heatmap .color-inactive {
+    fill: #eeeeee;
+    stroke-width: 1px;
+    stroke: #fcce9f;
+}
+
+.heatmap .color-active {
+    fill: #ff7f00;
+    stroke-width: 1px;
+    stroke: #ba5f02;
+}
+
+@keyframes createBox {
+    from {
+        width: 0;
+        height: 0;
+    }
+    to {
+        transform: scale(1);
+    }
+}
+
+.rect {
+    animation: createBox 1s;
+}
diff --git a/vinvoor/src/overview/heatmap/utils.ts b/vinvoor/src/overview/heatmap/utils.ts
new file mode 100644
index 0000000..efe2d1a
--- /dev/null
+++ b/vinvoor/src/overview/heatmap/utils.ts
@@ -0,0 +1,101 @@
+// Exports
+
+import { MILLISECONDS_IN_ONE_DAY } from "../../util/util";
+import { HeatmapVariant } from "./Heatmap";
+
+// Constants
+
+export const DAYS_IN_WEEK = 7;
+export const WEEKS_IN_MONTH = 5;
+export const SQUARE_SIZE = 10;
+
+export const MONTH_LABELS = [
+    "Jan",
+    "Feb",
+    "Mar",
+    "Apr",
+    "May",
+    "Jun",
+    "Jul",
+    "Aug",
+    "Sep",
+    "Oct",
+    "Nov",
+    "Dec",
+];
+export const dateTimeFormat = new Intl.DateTimeFormat("en-GB", {
+    year: "2-digit",
+    month: "short",
+    day: "numeric",
+});
+
+// Labels
+
+export const getMonthLabelSize = () => SQUARE_SIZE + MONTH_LABEL_GUTTER_SIZE;
+
+export const getMonthLabelCoordinates = (column: number) => [
+    column * getSquareSize(),
+    getMonthLabelSize() - MONTH_LABEL_GUTTER_SIZE,
+];
+
+// Transforms
+
+export const getTransformForColumn = (column: number) =>
+    `translate(${column * getSquareSize() + GUTTERSIZE}, 0)`;
+
+export const getTransformForAllWeeks = () =>
+    `translate(0, ${getMonthLabelSize()})`;
+
+export const getTransformForMonthLabels = () => `translate(0, 0)`;
+
+export const getWidth = (
+    startDate: Date,
+    endDate: Date,
+    variant: HeatmapVariant
+) => getColumnCount(startDate, endDate, variant) * getSquareSize() + GUTTERSIZE;
+
+export const getHeight = (variant: HeatmapVariant) => {
+    if (variant === HeatmapVariant.DAYS)
+        return DAYS_IN_WEEK * getSquareSize() + getMonthLabelSize();
+    else return WEEKS_IN_MONTH * getSquareSize() + getMonthLabelSize();
+};
+
+// Coordinate
+
+export const getSquareCoordinates = (dayIndex: number) => [
+    0,
+    dayIndex * getSquareSize(),
+];
+
+// Utils
+
+export const getEmpty = (date: Date, variant: HeatmapVariant) => {
+    if (variant === HeatmapVariant.DAYS)
+        return (date.getDay() + DAYS_IN_WEEK - 1) % DAYS_IN_WEEK;
+    else return Math.floor((date.getDate() - 1) / DAYS_IN_WEEK);
+};
+
+export const getColumnCount = (
+    startDate: Date,
+    endDate: Date,
+    variant: HeatmapVariant
+) => {
+    if (variant === HeatmapVariant.DAYS) {
+        return Math.ceil(
+            (endDate.getTime() - startDate.getTime()) /
+                (DAYS_IN_WEEK * MILLISECONDS_IN_ONE_DAY)
+        );
+    } else {
+        return (
+            (endDate.getFullYear() - startDate.getFullYear()) * 12 +
+            (endDate.getMonth() - startDate.getMonth() + 1)
+        );
+    }
+};
+
+// Local functions
+
+const GUTTERSIZE = 5;
+const MONTH_LABEL_GUTTER_SIZE = 8;
+
+const getSquareSize = () => SQUARE_SIZE + GUTTERSIZE;
diff --git a/vinvoor/src/overview/streak/Streak.tsx b/vinvoor/src/overview/streak/Streak.tsx
new file mode 100644
index 0000000..072f6b5
--- /dev/null
+++ b/vinvoor/src/overview/streak/Streak.tsx
@@ -0,0 +1,94 @@
+import { Box, Typography } from "@mui/material";
+import { useContext } from "react";
+import { Scan } from "../../types/scans";
+import {
+    isTheSameDay,
+    MILLISECONDS_IN_ONE_DAY,
+    shiftDate,
+} from "../../util/util";
+import { ScanContext } from "../Overview";
+
+const isWeekendBetween = (date1: Date, date2: Date) => {
+    const diffDays = Math.floor(
+        (date2.getTime() - date1.getTime()) / MILLISECONDS_IN_ONE_DAY
+    );
+
+    if (diffDays > 2) return false;
+
+    return date1.getDay() === 5 && [1, 6, 7].includes(date2.getDay());
+};
+
+const isStreakDay = (date1: Date, date2: Date) => {
+    if (isTheSameDay(date1, shiftDate(date2, 1))) return true;
+
+    if (date1.getDay() === 5 && [1, 6, 7].includes(date2.getDay()))
+        return isWeekendBetween(date1, date2);
+
+    return false;
+};
+
+const getStreak = (scans: readonly Scan[]): [boolean, number] => {
+    const dates = scans
+        .map((scan) => {
+            scan.scanTime.setHours(0, 0, 0, 0);
+            return scan.scanTime;
+        })
+        .filter((value, index, array) => {
+            return (
+                array.findIndex(
+                    (date) => date.getTime() === value.getTime()
+                ) === index
+            );
+        });
+    dates.sort((a, b) => a.getTime() - b.getTime());
+
+    let streak = 0;
+
+    const isOnStreak =
+        dates.length > 0 &&
+        (isTheSameDay(dates[dates.length - 1], new Date()) ||
+            isWeekendBetween(dates[dates.length - 1], new Date()));
+
+    if (isOnStreak) {
+        let i = dates.length;
+        streak++;
+
+        while (i-- > 1 && isStreakDay(dates[i], dates[i - 1])) streak++;
+    } else {
+        streak =
+            dates.length > 0
+                ? Math.floor(
+                      (new Date().getTime() -
+                          dates[dates.length - 1].getTime()) /
+                          MILLISECONDS_IN_ONE_DAY -
+                          1
+                  )
+                : 0;
+    }
+
+    return [isOnStreak, streak];
+};
+
+export const Streak = () => {
+    const { scans } = useContext(ScanContext);
+    let [isOnStreak, streak] = getStreak(scans);
+
+    const color = isOnStreak ? "primary" : "error";
+    const textEnd = isOnStreak ? "streak" : "absent";
+
+    return (
+        <Box display="flex" alignItems="flex-end" justifyContent="end">
+            <Typography
+                variant="h2"
+                color={color}
+                fontWeight="bold"
+                sx={{ mr: 1 }}
+            >
+                {streak}
+            </Typography>
+            <Typography variant="body2" color={color}>
+                day{streak > 1 ? "s" : ""} {textEnd}
+            </Typography>
+        </Box>
+    );
+};
diff --git a/vinvoor/src/scans/Scans.tsx b/vinvoor/src/scans/Scans.tsx
new file mode 100644
index 0000000..ced950f
--- /dev/null
+++ b/vinvoor/src/scans/Scans.tsx
@@ -0,0 +1,36 @@
+import { Paper, Table, TableContainer } from "@mui/material";
+import { useState } from "react";
+import { LoadingSkeleton } from "../components/LoadingSkeleton";
+import { useFetch } from "../hooks/useFetch";
+import { Card, convertCardJSON } from "../types/cards";
+import { convertScanJSON, Scan } from "../types/scans";
+import { ScansTableBody } from "./ScansBody";
+import { ScansTableHead } from "./ScansTableHead";
+
+export const Scans = () => {
+    const [scans, setScans] = useState<readonly Scan[]>([]);
+    const [cards, setCards] = useState<readonly Card[]>([]);
+    const { loading: loadingScans } = useFetch<readonly Scan[]>(
+        "scans",
+        setScans,
+        convertScanJSON
+    );
+    const { loading: loadingCards } = useFetch<readonly Card[]>(
+        "cards",
+        setCards,
+        convertCardJSON
+    );
+
+    return (
+        <LoadingSkeleton loading={loadingScans && loadingCards}>
+            <Paper elevation={4}>
+                <TableContainer>
+                    <Table>
+                        <ScansTableHead />
+                        <ScansTableBody scans={scans} cards={cards} />
+                    </Table>
+                </TableContainer>
+            </Paper>
+        </LoadingSkeleton>
+    );
+};
diff --git a/vinvoor/src/scans/ScansBody.tsx b/vinvoor/src/scans/ScansBody.tsx
new file mode 100644
index 0000000..588a07d
--- /dev/null
+++ b/vinvoor/src/scans/ScansBody.tsx
@@ -0,0 +1,43 @@
+import { TableBody, TableCell, TableRow, Typography } from "@mui/material";
+import { FC, useEffect, useState } from "react";
+import { Card } from "../types/cards";
+import {
+    mergeScansCards,
+    Scan,
+    ScanCard,
+    scanCardHeadCells,
+} from "../types/scans";
+
+interface ScansTableBodyProps {
+    scans: readonly Scan[];
+    cards: readonly Card[];
+}
+
+export const ScansTableBody: FC<ScansTableBodyProps> = ({ scans, cards }) => {
+    const [scanCards, setScanCards] = useState<readonly ScanCard[]>([]);
+
+    useEffect(() => {
+        setScanCards(mergeScansCards(scans, cards));
+    }, [scans, cards]);
+
+    return (
+        <TableBody>
+            {scanCards.map((scanCard, index) => (
+                <TableRow key={index}>
+                    {scanCardHeadCells.map((headCell) => (
+                        <TableCell
+                            key={headCell.id}
+                            align={headCell.align}
+                            padding={headCell.padding}
+                        >
+                            <Typography>
+                                {headCell.convert &&
+                                    headCell.convert(scanCard[headCell.id])}
+                            </Typography>
+                        </TableCell>
+                    ))}
+                </TableRow>
+            ))}
+        </TableBody>
+    );
+};
diff --git a/vinvoor/src/leaderboard/LeaderboardTableHead.tsx b/vinvoor/src/scans/ScansTableHead.tsx
similarity index 69%
rename from vinvoor/src/leaderboard/LeaderboardTableHead.tsx
rename to vinvoor/src/scans/ScansTableHead.tsx
index 9f44f14..1ca025b 100644
--- a/vinvoor/src/leaderboard/LeaderboardTableHead.tsx
+++ b/vinvoor/src/scans/ScansTableHead.tsx
@@ -1,11 +1,11 @@
 import { TableCell, TableHead, TableRow, Typography } from "@mui/material";
-import { leaderboardHeadCells } from "../types/leaderboard";
+import { scanCardHeadCells } from "../types/scans";
 
-export const LeaderboardTableHead = () => {
+export const ScansTableHead = () => {
     return (
         <TableHead>
             <TableRow>
-                {leaderboardHeadCells.map((headCell) => (
+                {scanCardHeadCells.map((headCell) => (
                     <TableCell key={headCell.id} align={headCell.align}>
                         <Typography>{headCell.label}</Typography>
                     </TableCell>
diff --git a/vinvoor/src/settings/Settings.tsx b/vinvoor/src/settings/Settings.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/vinvoor/src/types/cards.ts b/vinvoor/src/types/cards.ts
index 223a1d3..e406160 100644
--- a/vinvoor/src/types/cards.ts
+++ b/vinvoor/src/types/cards.ts
@@ -1,21 +1,75 @@
-import { TableHeadCell } from "./table";
+import { Base, BaseJSON, TableHeadCell } from "./general";
 
-export interface Card {
+interface CardJSON extends BaseJSON {
     serial: string;
-    createdAt: string;
+    name: string;
+    lastUsed: string;
+    amountUsed: number;
 }
 
-export const CardsHeadCells: readonly TableHeadCell<Card>[] = [
+export interface Card extends Base {
+    serial: string;
+    name: string;
+    lastUsed: Date;
+    amountUsed: number;
+}
+
+export const convertCardJSON = (cardsJSON: CardJSON[]): Card[] =>
+    cardsJSON.map((CardJSON) => ({
+        serial: CardJSON.serial,
+        name: CardJSON.name,
+        lastUsed: new Date(CardJSON.lastUsed),
+        amountUsed: CardJSON.amountUsed,
+        id: CardJSON.id,
+        createdAt: new Date(CardJSON.createdAt),
+    }));
+
+export const cardsHeadCells: readonly TableHeadCell<Card>[] = [
     {
-        id: "serial",
-        label: "Serial",
+        id: "name",
+        label: "Name",
         align: "left",
         padding: "none",
     },
+    {
+        id: "amountUsed",
+        label: "Amount of uses",
+        align: "right",
+        padding: "none",
+    },
+    {
+        id: "lastUsed",
+        label: "Last used",
+        align: "right",
+        padding: "normal",
+        convert: (value: Date) => {
+            if (value.getFullYear() === 1) return "Not used";
+            else return value.toDateString();
+        },
+    },
     {
         id: "createdAt",
         label: "Created at",
         align: "right",
         padding: "normal",
+        convert: (value: Date) => value.toDateString(),
+    },
+    {
+        id: "serial",
+        label: "Serial",
+        align: "right",
+        padding: "normal",
     },
 ];
+
+export interface CardPostResponse {
+    isCurrentUser: boolean;
+}
+
+export interface CardGetRegisterResponse {
+    registering: boolean;
+    isCurrentUser: boolean;
+    success: boolean;
+    timeRemaining: number;
+    timePercentage: number;
+}
diff --git a/vinvoor/src/types/table.ts b/vinvoor/src/types/general.ts
similarity index 63%
rename from vinvoor/src/types/table.ts
rename to vinvoor/src/types/general.ts
index 3d4a059..99dc2ce 100644
--- a/vinvoor/src/types/table.ts
+++ b/vinvoor/src/types/general.ts
@@ -1,3 +1,13 @@
+export interface BaseJSON {
+    id: number;
+    createdAt: string;
+}
+
+export interface Base {
+    id: number;
+    createdAt: Date;
+}
+
 export type TableOrder = "asc" | "desc";
 
 type TableAlignOptions = "right" | "left" | "center";
@@ -8,4 +18,5 @@ export interface TableHeadCell<T> {
     label: string;
     align: TableAlignOptions;
     padding: TablePaddingOptions;
+    convert?: (value: any) => string;
 }
diff --git a/vinvoor/src/types/leaderboard.ts b/vinvoor/src/types/leaderboard.ts
index 4493005..d1899c1 100644
--- a/vinvoor/src/types/leaderboard.ts
+++ b/vinvoor/src/types/leaderboard.ts
@@ -1,12 +1,20 @@
-import { TableHeadCell } from "./table";
+import { TableHeadCell } from "./general";
 
 export interface LeaderboardItem {
     position: number;
+    userId: number;
     username: string;
     totalDays: number;
+    positionChange: number;
 }
 
 export const leaderboardHeadCells: readonly TableHeadCell<LeaderboardItem>[] = [
+    {
+        id: "positionChange",
+        label: "Change",
+        align: "left",
+        padding: "checkbox",
+    },
     {
         id: "position",
         label: "#",
diff --git a/vinvoor/src/types/scans.ts b/vinvoor/src/types/scans.ts
new file mode 100644
index 0000000..285e8a8
--- /dev/null
+++ b/vinvoor/src/types/scans.ts
@@ -0,0 +1,52 @@
+import { Card } from "./cards";
+import { TableHeadCell } from "./general";
+
+interface ScanJSON {
+    scanTime: string;
+    cardSerial: string;
+}
+
+export interface Scan {
+    scanTime: Date;
+    cardSerial: string;
+}
+
+export interface ScanCard {
+    scanTime: Date;
+    card?: Card;
+}
+
+export const convertScanJSON = (scansJSON: ScanJSON[]): Scan[] =>
+    scansJSON
+        .map((scanJSON) => ({
+            scanTime: new Date(scanJSON.scanTime),
+            cardSerial: scanJSON.cardSerial,
+        }))
+        .sort((a, b) => a.scanTime.getTime() - b.scanTime.getTime());
+
+export const mergeScansCards = (
+    scans: readonly Scan[],
+    cards: readonly Card[]
+): readonly ScanCard[] =>
+    scans.map((scan) => ({
+        scanTime: scan.scanTime,
+        card: cards.find((card) => card.serial === scan.cardSerial),
+    }));
+
+export const scanCardHeadCells: readonly TableHeadCell<ScanCard>[] = [
+    {
+        id: "scanTime",
+        label: "Scan time",
+        align: "left",
+        padding: "normal",
+        convert: (value: Date) => value.toDateString(),
+    },
+    {
+        id: "card",
+        label: "Card",
+        align: "right",
+        padding: "normal",
+        convert: (value: Card | undefined) =>
+            value?.name || (value?.serial ?? "Unknown"),
+    },
+];
diff --git a/vinvoor/src/user/Login.tsx b/vinvoor/src/user/Login.tsx
index 97276ee..6a0ac81 100644
--- a/vinvoor/src/user/Login.tsx
+++ b/vinvoor/src/user/Login.tsx
@@ -1,11 +1,16 @@
-import { useEffect } from "react";
+import { Button, ButtonProps } from "@mui/material";
+import { FC } from "react";
 
-export const Login = () => {
+export const Login: FC<ButtonProps> = (props) => {
     const baseUrl = import.meta.env.VITE_BASE_URL;
 
-    useEffect(() => {
-        window.location.replace(`${baseUrl}/login`);
-    }, []);
+    const handleClick = () => {
+        const form = document.createElement("form");
+        form.method = "POST";
+        form.action = `${baseUrl}/login`;
+        document.body.appendChild(form);
+        form.submit();
+    };
 
-    return <></>;
+    return <Button onClick={handleClick} {...props} />;
 };
diff --git a/vinvoor/src/user/Logout.tsx b/vinvoor/src/user/Logout.tsx
index f4b8222..fdd1476 100644
--- a/vinvoor/src/user/Logout.tsx
+++ b/vinvoor/src/user/Logout.tsx
@@ -1,11 +1,16 @@
-import { useEffect } from "react";
+import { Button, ButtonProps } from "@mui/material";
+import { FC } from "react";
 
-export const Logout = () => {
-    const baseUrl = import.meta.env.VITE_BASE_URL;
+export const Logout: FC<ButtonProps> = (props) => {
+    const apiUrl = import.meta.env.VITE_API_URL;
 
-    useEffect(() => {
-        window.location.replace(`${baseUrl}/logout`);
-    }, []);
+    const handleClick = () => {
+        const form = document.createElement("form");
+        form.method = "POST";
+        form.action = `${apiUrl}/logout`;
+        document.body.appendChild(form);
+        form.submit();
+    };
 
-    return <></>;
+    return <Button onClick={handleClick} {...props} />;
 };
diff --git a/vinvoor/src/user/UserProvider.tsx b/vinvoor/src/user/UserProvider.tsx
index 7577470..f145e97 100644
--- a/vinvoor/src/user/UserProvider.tsx
+++ b/vinvoor/src/user/UserProvider.tsx
@@ -9,7 +9,7 @@ import {
     useState,
 } from "react";
 import { User } from "../types/user";
-import { fetchApi } from "../util/fetch";
+import { getApi } from "../util/fetch";
 
 interface UserProviderProps {
     children: ReactNode;
@@ -55,7 +55,7 @@ export const UserProvider: FC<UserProviderProps> = ({ children }) => {
 
         let newUserState = { ...userState };
 
-        fetchApi("user")
+        getApi<User>("user")
             .then((data) => (newUserState.user = data))
             .catch((error) => {
                 Cookies.remove("session_id");
diff --git a/vinvoor/src/util/fetch.ts b/vinvoor/src/util/fetch.ts
index 0497de2..dbfe04e 100644
--- a/vinvoor/src/util/fetch.ts
+++ b/vinvoor/src/util/fetch.ts
@@ -3,16 +3,58 @@ const URLS: { [key: string]: string } = {
     API: import.meta.env.VITE_API_URL,
 };
 
-export const fetchBase = (endpoint: string) => {
-    return _fetch(`${URLS.BASE}/${endpoint}`);
+export const getApi = <T>(endpoint: string, convertData?: (data: any) => T) => {
+    return _fetch<T>(`${URLS.API}/${endpoint}`, {}, convertData);
 };
 
-export const fetchApi = (endpoint: string) => {
-    return _fetch(`${URLS.API}/${endpoint}`);
+export const postApi = <T>(
+    endpoint: string,
+    body: { [key: string]: string } = {}
+) => {
+    return _fetch<T>(`${URLS.API}/${endpoint}`, {
+        method: "POST",
+        body: JSON.stringify(body),
+        headers: new Headers({ "content-type": "application/json" }),
+    });
 };
 
-const _fetch = async (url: string) => {
-    return fetch(url, { credentials: "include" }).then((response) =>
-        response.json()
-    );
+export const patchApi = <T>(
+    endpoint: string,
+    body: { [key: string]: string } = {}
+) => {
+    return _fetch<T>(`${URLS.API}/${endpoint}`, {
+        method: "PATCH",
+        body: JSON.stringify(body),
+        headers: new Headers({ "content-type": "application/json" }),
+    });
+};
+
+interface ResponseNot200Error extends Error {
+    response: Response;
+}
+
+export const isResponseNot200Error = (
+    error: any
+): error is ResponseNot200Error => {
+    return (error as ResponseNot200Error).response !== undefined;
+};
+
+const _fetch = async <T>(
+    url: string,
+    options: RequestInit = {},
+    convertData?: (data: any) => T
+): Promise<T> => {
+    return fetch(url, { credentials: "include", ...options })
+        .then((response) => {
+            if (!response.ok) {
+                const error = new Error(
+                    "Fetch failed with status: " + response.status
+                ) as ResponseNot200Error;
+                error.response = response;
+                throw error;
+            }
+
+            return response.json();
+        })
+        .then((data) => (convertData ? convertData(data) : data));
 };
diff --git a/vinvoor/src/util/util.ts b/vinvoor/src/util/util.ts
new file mode 100644
index 0000000..4f92678
--- /dev/null
+++ b/vinvoor/src/util/util.ts
@@ -0,0 +1,60 @@
+export const randomInt = (lower: number = 0, upper: number = 10000): number => {
+    return Math.floor(Math.random() * (upper - lower + 1) + lower);
+};
+
+// Date functions
+
+export const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000;
+
+export const isTheSameDay = (date1: Date, date2: Date) =>
+    date1.getFullYear() === date2.getFullYear() &&
+    date1.getMonth() === date2.getMonth() &&
+    date1.getDate() === date2.getDate();
+
+export const shiftDate = (date: Date, numDays: number) => {
+    const newDate = new Date(date);
+    newDate.setDate(newDate.getDate() + numDays);
+    return newDate;
+};
+
+// Compare functions
+
+export const equal = (left: any, right: any): boolean => {
+    if (typeof left !== typeof right) return false;
+
+    if (Array.isArray(left) && Array.isArray(right))
+        return equalArray(left, right);
+    if (typeof left === "object" && left !== null && right !== null)
+        return equalObject(
+            left as Record<string, any>,
+            right as Record<string, any>
+        );
+
+    return left === right;
+};
+
+const equalArray = (left: any[], right: any[]): boolean => {
+    if (left.length !== right.length) return false;
+
+    for (let i = 0; i < left.length; i++) {
+        if (!equal(left[i], right[i])) return false;
+    }
+
+    return true;
+};
+
+const equalObject = (
+    left: Record<string, any>,
+    right: Record<string, any>
+): boolean => {
+    const leftKeys = Object.keys(left);
+    const rightKeys = Object.keys(right);
+
+    if (leftKeys.length !== rightKeys.length) return false;
+
+    for (const key of leftKeys) {
+        if (!equal(left[key], right[key])) return false;
+    }
+
+    return true;
+};
diff --git a/vinvoor/vite.config.ts b/vinvoor/vite.config.ts
index 03d504e..25b3d61 100644
--- a/vinvoor/vite.config.ts
+++ b/vinvoor/vite.config.ts
@@ -1,7 +1,8 @@
 import react from "@vitejs/plugin-react-swc";
 import { defineConfig } from "vite";
+import svgr from "vite-plugin-svgr";
 
 // https://vitejs.dev/config/
 export default defineConfig({
-    plugins: [react()],
+    plugins: [svgr(), react()],
 });
diff --git a/vinvoor/yarn.lock b/vinvoor/yarn.lock
index 78fe7ca..8a3865f 100644
--- a/vinvoor/yarn.lock
+++ b/vinvoor/yarn.lock
@@ -2,6 +2,14 @@
 # yarn lockfile v1
 
 
+"@ampproject/remapping@^2.2.0":
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
+  integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.3.5"
+    "@jridgewell/trace-mapping" "^0.3.24"
+
 "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.24.7":
   version "7.24.7"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465"
@@ -10,6 +18,32 @@
     "@babel/highlight" "^7.24.7"
     picocolors "^1.0.0"
 
+"@babel/compat-data@^7.24.7":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed"
+  integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==
+
+"@babel/core@^7.21.3":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4"
+  integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==
+  dependencies:
+    "@ampproject/remapping" "^2.2.0"
+    "@babel/code-frame" "^7.24.7"
+    "@babel/generator" "^7.24.7"
+    "@babel/helper-compilation-targets" "^7.24.7"
+    "@babel/helper-module-transforms" "^7.24.7"
+    "@babel/helpers" "^7.24.7"
+    "@babel/parser" "^7.24.7"
+    "@babel/template" "^7.24.7"
+    "@babel/traverse" "^7.24.7"
+    "@babel/types" "^7.24.7"
+    convert-source-map "^2.0.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.2"
+    json5 "^2.2.3"
+    semver "^6.3.1"
+
 "@babel/generator@^7.24.7":
   version "7.24.7"
   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d"
@@ -20,6 +54,17 @@
     "@jridgewell/trace-mapping" "^0.3.25"
     jsesc "^2.5.1"
 
+"@babel/helper-compilation-targets@^7.24.7":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz#4eb6c4a80d6ffeac25ab8cd9a21b5dfa48d503a9"
+  integrity sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==
+  dependencies:
+    "@babel/compat-data" "^7.24.7"
+    "@babel/helper-validator-option" "^7.24.7"
+    browserslist "^4.22.2"
+    lru-cache "^5.1.1"
+    semver "^6.3.1"
+
 "@babel/helper-environment-visitor@^7.24.7":
   version "7.24.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9"
@@ -42,7 +87,7 @@
   dependencies:
     "@babel/types" "^7.24.7"
 
-"@babel/helper-module-imports@^7.16.7":
+"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.24.7":
   version "7.24.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b"
   integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==
@@ -50,6 +95,25 @@
     "@babel/traverse" "^7.24.7"
     "@babel/types" "^7.24.7"
 
+"@babel/helper-module-transforms@^7.24.7":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8"
+  integrity sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==
+  dependencies:
+    "@babel/helper-environment-visitor" "^7.24.7"
+    "@babel/helper-module-imports" "^7.24.7"
+    "@babel/helper-simple-access" "^7.24.7"
+    "@babel/helper-split-export-declaration" "^7.24.7"
+    "@babel/helper-validator-identifier" "^7.24.7"
+
+"@babel/helper-simple-access@^7.24.7":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3"
+  integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==
+  dependencies:
+    "@babel/traverse" "^7.24.7"
+    "@babel/types" "^7.24.7"
+
 "@babel/helper-split-export-declaration@^7.24.7":
   version "7.24.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856"
@@ -67,6 +131,19 @@
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db"
   integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
 
+"@babel/helper-validator-option@^7.24.7":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6"
+  integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==
+
+"@babel/helpers@^7.24.7":
+  version "7.24.7"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.7.tgz#aa2ccda29f62185acb5d42fb4a3a1b1082107416"
+  integrity sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==
+  dependencies:
+    "@babel/template" "^7.24.7"
+    "@babel/types" "^7.24.7"
+
 "@babel/highlight@^7.24.7":
   version "7.24.7"
   resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d"
@@ -114,7 +191,7 @@
     debug "^4.3.1"
     globals "^11.1.0"
 
-"@babel/types@^7.24.7":
+"@babel/types@^7.21.3", "@babel/types@^7.24.7":
   version "7.24.7"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2"
   integrity sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==
@@ -230,120 +307,120 @@
   resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6"
   integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==
 
-"@esbuild/aix-ppc64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
-  integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==
-
-"@esbuild/android-arm64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9"
-  integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==
-
-"@esbuild/android-arm@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995"
-  integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==
-
-"@esbuild/android-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98"
-  integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==
-
-"@esbuild/darwin-arm64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb"
-  integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==
-
-"@esbuild/darwin-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0"
-  integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==
-
-"@esbuild/freebsd-arm64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911"
-  integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==
-
-"@esbuild/freebsd-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c"
-  integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==
-
-"@esbuild/linux-arm64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5"
-  integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==
-
-"@esbuild/linux-arm@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c"
-  integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==
-
-"@esbuild/linux-ia32@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa"
-  integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==
-
-"@esbuild/linux-loong64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5"
-  integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==
-
-"@esbuild/linux-mips64el@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa"
-  integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==
-
-"@esbuild/linux-ppc64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20"
-  integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==
-
-"@esbuild/linux-riscv64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300"
-  integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==
-
-"@esbuild/linux-s390x@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685"
-  integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==
-
-"@esbuild/linux-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff"
-  integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==
-
-"@esbuild/netbsd-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6"
-  integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==
-
-"@esbuild/openbsd-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf"
-  integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==
-
-"@esbuild/sunos-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f"
-  integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==
-
-"@esbuild/win32-arm64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90"
-  integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==
-
-"@esbuild/win32-ia32@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23"
-  integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==
-
-"@esbuild/win32-x64@0.20.2":
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
-  integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
+"@esbuild/aix-ppc64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
+  integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
+
+"@esbuild/android-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
+  integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
+
+"@esbuild/android-arm@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
+  integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
+
+"@esbuild/android-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
+  integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
+
+"@esbuild/darwin-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
+  integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
+
+"@esbuild/darwin-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
+  integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
+
+"@esbuild/freebsd-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
+  integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
+
+"@esbuild/freebsd-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
+  integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
+
+"@esbuild/linux-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
+  integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
+
+"@esbuild/linux-arm@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
+  integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
+
+"@esbuild/linux-ia32@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
+  integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
+
+"@esbuild/linux-loong64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
+  integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
+
+"@esbuild/linux-mips64el@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
+  integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
+
+"@esbuild/linux-ppc64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
+  integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
+
+"@esbuild/linux-riscv64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
+  integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
+
+"@esbuild/linux-s390x@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
+  integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
+
+"@esbuild/linux-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
+  integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
+
+"@esbuild/netbsd-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
+  integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
+
+"@esbuild/openbsd-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
+  integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
+
+"@esbuild/sunos-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
+  integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
+
+"@esbuild/win32-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
+  integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
+
+"@esbuild/win32-ia32@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
+  integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
+
+"@esbuild/win32-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
+  integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
 
 "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
   version "4.4.0"
@@ -353,9 +430,9 @@
     eslint-visitor-keys "^3.3.0"
 
 "@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1":
-  version "4.10.1"
-  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.1.tgz#361461e5cb3845d874e61731c11cfedd664d83a0"
-  integrity sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==
+  version "4.11.0"
+  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae"
+  integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==
 
 "@eslint/eslintrc@^2.1.4":
   version "2.1.4"
@@ -377,32 +454,32 @@
   resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
   integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
 
-"@floating-ui/core@^1.0.0":
-  version "1.6.2"
-  resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.2.tgz#d37f3e0ac1f1c756c7de45db13303a266226851a"
-  integrity sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==
+"@floating-ui/core@^1.6.0":
+  version "1.6.4"
+  resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.4.tgz#0140cf5091c8dee602bff9da5ab330840ff91df6"
+  integrity sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==
   dependencies:
-    "@floating-ui/utils" "^0.2.0"
+    "@floating-ui/utils" "^0.2.4"
 
-"@floating-ui/dom@^1.0.0":
-  version "1.6.5"
-  resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.5.tgz#323f065c003f1d3ecf0ff16d2c2c4d38979f4cb9"
-  integrity sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==
+"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.6.1":
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.7.tgz#85d22f731fcc5b209db504478fb1df5116a83015"
+  integrity sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==
   dependencies:
-    "@floating-ui/core" "^1.0.0"
-    "@floating-ui/utils" "^0.2.0"
+    "@floating-ui/core" "^1.6.0"
+    "@floating-ui/utils" "^0.2.4"
 
 "@floating-ui/react-dom@^2.0.8":
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.0.tgz#4f0e5e9920137874b2405f7d6c862873baf4beff"
-  integrity sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.1.tgz#cca58b6b04fc92b4c39288252e285e0422291fb0"
+  integrity sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==
   dependencies:
     "@floating-ui/dom" "^1.0.0"
 
-"@floating-ui/utils@^0.2.0":
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5"
-  integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==
+"@floating-ui/utils@^0.2.4":
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.4.tgz#1d459cee5031893a08a0e064c406ad2130cced7c"
+  integrity sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==
 
 "@fontsource/roboto@^5.0.13":
   version "5.0.13"
@@ -473,29 +550,29 @@
     clsx "^2.1.0"
     prop-types "^15.8.1"
 
-"@mui/core-downloads-tracker@^5.15.19":
-  version "5.15.19"
-  resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.19.tgz#7af0025c871f126367a55219486681954e4821d7"
-  integrity sha512-tCHSi/Tomez9ERynFhZRvFO6n9ATyrPs+2N80DMDzp6xDVirbBjEwhPcE+x7Lj+nwYw0SqFkOxyvMP0irnm55w==
+"@mui/core-downloads-tracker@^5.16.0":
+  version "5.16.0"
+  resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.0.tgz#50153c698e321793c83a0283d8d7a9dc5d43858a"
+  integrity sha512-8SLffXYPRVpcZx5QzxNE8fytTqzp+IuU3deZbQWg/vSaTlDpR5YVrQ4qQtXTi5cRdhOufV5INylmwlKK+//nPw==
 
 "@mui/icons-material@^5.15.19":
-  version "5.15.19"
-  resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.15.19.tgz#0602da80d814af662812659eab891e435ec0d5c0"
-  integrity sha512-RsEiRxA5azN9b8gI7JRqekkgvxQUlitoBOtZglflb8cUDyP12/cP4gRwhb44Ea1/zwwGGjAj66ZJpGHhKfibNA==
+  version "5.16.0"
+  resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.16.0.tgz#5269fda922fe5e6db3577ec497e8b987195606ef"
+  integrity sha512-6ISoOhkp9w5gD0PEW9JklrcbyARDkFWNTBdwXZ1Oy5IGlyu9B0zG0hnUIe4H17IaF1Vgj6C8VI+v4tkSdK0veg==
   dependencies:
     "@babel/runtime" "^7.23.9"
 
 "@mui/material@^5.15.19":
-  version "5.15.19"
-  resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.15.19.tgz#a5bd50b6e68cee4ed39ea91dbecede5a020aaa97"
-  integrity sha512-lp5xQBbcRuxNtjpWU0BWZgIrv2XLUz4RJ0RqFXBdESIsKoGCQZ6P3wwU5ZPuj5TjssNiKv9AlM+vHopRxZhvVQ==
+  version "5.16.0"
+  resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.16.0.tgz#2ef4f52ae773574fc0a681f25705f376f5cd13f7"
+  integrity sha512-DbR1NckTLpjt9Zut9EGQ70th86HfN0BYQgyYro6aXQrNfjzSwe3BJS1AyBQ5mJ7TdL6YVRqohfukxj9JlqZZUg==
   dependencies:
     "@babel/runtime" "^7.23.9"
     "@mui/base" "5.0.0-beta.40"
-    "@mui/core-downloads-tracker" "^5.15.19"
-    "@mui/system" "^5.15.15"
+    "@mui/core-downloads-tracker" "^5.16.0"
+    "@mui/system" "^5.16.0"
     "@mui/types" "^7.2.14"
-    "@mui/utils" "^5.15.14"
+    "@mui/utils" "^5.16.0"
     "@types/react-transition-group" "^4.4.10"
     clsx "^2.1.0"
     csstype "^3.1.3"
@@ -503,13 +580,13 @@
     react-is "^18.2.0"
     react-transition-group "^4.4.5"
 
-"@mui/private-theming@^5.15.14":
-  version "5.15.14"
-  resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.15.14.tgz#edd9a82948ed01586a01c842eb89f0e3f68970ee"
-  integrity sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==
+"@mui/private-theming@^5.16.0":
+  version "5.16.0"
+  resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.16.0.tgz#c1abfd3e0d9c95459048240ef4209dc7f25dc949"
+  integrity sha512-sYpubkO1MZOnxNyVOClrPNOTs0MfuRVVnAvCeMaOaXt6GimgQbnUcshYv2pSr6PFj+Mqzdff/FYOBceK8u5QgA==
   dependencies:
     "@babel/runtime" "^7.23.9"
-    "@mui/utils" "^5.15.14"
+    "@mui/utils" "^5.16.0"
     prop-types "^15.8.1"
 
 "@mui/styled-engine@^5.15.14":
@@ -522,16 +599,16 @@
     csstype "^3.1.3"
     prop-types "^15.8.1"
 
-"@mui/system@^5.15.15":
-  version "5.15.15"
-  resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.15.15.tgz#658771b200ce3c4a0f28e58169f02e5e718d1c53"
-  integrity sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==
+"@mui/system@^5.16.0":
+  version "5.16.0"
+  resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.16.0.tgz#e5b4cfbdfbc0ee9859f6b168e8b07d750303b7a0"
+  integrity sha512-9YbkC2m3+pNumAvubYv+ijLtog6puJ0fJ6rYfzfLCM47pWrw3m+30nXNM8zMgDaKL6vpfWJcCXm+LPaWBpy7sw==
   dependencies:
     "@babel/runtime" "^7.23.9"
-    "@mui/private-theming" "^5.15.14"
+    "@mui/private-theming" "^5.16.0"
     "@mui/styled-engine" "^5.15.14"
     "@mui/types" "^7.2.14"
-    "@mui/utils" "^5.15.14"
+    "@mui/utils" "^5.16.0"
     clsx "^2.1.0"
     csstype "^3.1.3"
     prop-types "^15.8.1"
@@ -541,10 +618,10 @@
   resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.14.tgz#8a02ac129b70f3d82f2f9b76ded2c8d48e3fc8c9"
   integrity sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==
 
-"@mui/utils@^5.15.14":
-  version "5.15.14"
-  resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.14.tgz#e414d7efd5db00bfdc875273a40c0a89112ade3a"
-  integrity sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==
+"@mui/utils@^5.15.14", "@mui/utils@^5.16.0":
+  version "5.16.0"
+  resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.16.0.tgz#3963127d9a619c251e5be1aef9adab0e89d3e7df"
+  integrity sha512-kLLi5J1xY+mwtUlMb8Ubdxf4qFAA1+U7WPBvjM/qQ4CIwLCohNb0sHo1oYPufjSIH/Z9+dhVxD7dJlfGjd1AVA==
   dependencies:
     "@babel/runtime" "^7.23.9"
     "@types/prop-types" "^15.7.11"
@@ -577,10 +654,19 @@
   resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
   integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
 
-"@remix-run/router@1.16.1":
-  version "1.16.1"
-  resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.16.1.tgz#73db3c48b975eeb06d0006481bde4f5f2d17d1cd"
-  integrity sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==
+"@remix-run/router@1.17.1":
+  version "1.17.1"
+  resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.17.1.tgz#bf93997beb81863fde042ebd05013a2618471362"
+  integrity sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==
+
+"@rollup/pluginutils@^5.0.5":
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0"
+  integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==
+  dependencies:
+    "@types/estree" "^1.0.0"
+    estree-walker "^2.0.2"
+    picomatch "^2.3.1"
 
 "@rollup/rollup-android-arm-eabi@4.18.0":
   version "4.18.0"
@@ -662,88 +748,171 @@
   resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4"
   integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==
 
-"@swc/core-darwin-arm64@1.5.27":
-  version "1.5.27"
-  resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.27.tgz#659f01c7687d5615785cabecab0f985a4e433326"
-  integrity sha512-jyoygXBcUcwUya2BI7Uvl0jwcm4kd0RBDGGkWgcFAZmwucSuLT3EsbpWhOwlL3ACT4rpnRlvh+k8nJlq3+w2Aw==
-
-"@swc/core-darwin-x64@1.5.27":
-  version "1.5.27"
-  resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.5.27.tgz#561e5ddeba0621ccd86445904327b5ac66887be6"
-  integrity sha512-eOC583D6b3MS9oODCcZUvAV7ajunjENAPVQL7aZaW+piERW+o4koZAiPlzFdMAUMj7UeVg+UN9sBBbTbJgruIA==
-
-"@swc/core-linux-arm-gnueabihf@1.5.27":
-  version "1.5.27"
-  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.27.tgz#b7d5e2504bc66f1b93766d8d2c3b6de6b6bb3821"
-  integrity sha512-bMvX0yF7WYzn1K+s0JWJhvyA3OeZHVrdjka8eZ4LSeuLfC0ggJefo+klyeuN2szn/LYP6/3oByyrWNY8RSHB4w==
-
-"@swc/core-linux-arm64-gnu@1.5.27":
-  version "1.5.27"
-  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.27.tgz#8418e0e0971e8ee5ad40bec940c1aa7e9569862b"
-  integrity sha512-KlkOcSPxrCqZTm4XrT/LT1o9gmyM2T6bw/hL6IwTYRBJg+sej4rc9iSfVRFZBfNuG3EVkFQSXxik+0yVOXR93Q==
-
-"@swc/core-linux-arm64-musl@1.5.27":
-  version "1.5.27"
-  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.27.tgz#f4c20b5f3c2b008d5d469d424d4f7de4f308ceea"
-  integrity sha512-EwdTt5qykxFXJu7kS+0X0Mp/IlwO8KJ6LVNak2+N8bt1J1q/nCdg1tRDOYQ1Z/MVa1Tm+lJ664Qs1y2NY2SDTw==
-
-"@swc/core-linux-x64-gnu@1.5.27":
-  version "1.5.27"
-  resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.27.tgz#27585a3cb38cab77dd62aaba7eba811fa384e91e"
-  integrity sha512-RsBbxbiSNWLJ2jbAdITtv30J4eZw4O/JJ5zxYgWI54TdY7YrVsqIdzuX+ldximt+CYvO9irHm/mSr/IJY2YXrw==
-
-"@swc/core-linux-x64-musl@1.5.27":
-  version "1.5.27"
-  resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.27.tgz#a9041d19e3770ef2c9c31c3ef30798ee4949d076"
-  integrity sha512-XpRx0Kpy6JEi1WSMqUfR3k8hXLqNOkVqFcUfzvfQ4QNBX5Ek7ywh7WAxlPhCrFp+wAfNAqqUyRY1xZpLvRU51A==
-
-"@swc/core-win32-arm64-msvc@1.5.27":
-  version "1.5.27"
-  resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.27.tgz#cb05cd9ed099676a6c26251f1fd170e346e89cb5"
-  integrity sha512-pwSTUIokyIp+Ha1pur34qdYjxqL1QzhP/HM8anzsFs4yvV2LSI7c3qc4GWPNv2eQ9WiFXyo29uCEIRN6qig7wg==
-
-"@swc/core-win32-ia32-msvc@1.5.27":
-  version "1.5.27"
-  resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.27.tgz#96e4ac203df0dd07bb8bb1125c280fb5feccadef"
-  integrity sha512-S0S6vqFscvmxPolwmpZvTRfTbYR+eGcyc0ge4x/+HcnBCm+m84rcGxmp3bBb1edNFaIV+X47BlGvvh85cJ4rkQ==
-
-"@swc/core-win32-x64-msvc@1.5.27":
-  version "1.5.27"
-  resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.27.tgz#78d7c230d3dd442de72beb06187e76471703a11d"
-  integrity sha512-aem+BcNW42JPbvV6L3Jl3LLj6G80aYADzYenToYisy0Aop0XZAxL/0FbhV+xWORNFtIUKynNtaa1CK7w0UxehQ==
+"@svgr/babel-plugin-add-jsx-attribute@8.0.0":
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22"
+  integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==
+
+"@svgr/babel-plugin-remove-jsx-attribute@8.0.0":
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186"
+  integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==
+
+"@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0":
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44"
+  integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==
+
+"@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0":
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz#8fbb6b2e91fa26ac5d4aa25c6b6e4f20f9c0ae27"
+  integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==
+
+"@svgr/babel-plugin-svg-dynamic-title@8.0.0":
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz#1d5ba1d281363fc0f2f29a60d6d936f9bbc657b0"
+  integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==
+
+"@svgr/babel-plugin-svg-em-dimensions@8.0.0":
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz#35e08df300ea8b1d41cb8f62309c241b0369e501"
+  integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==
+
+"@svgr/babel-plugin-transform-react-native-svg@8.1.0":
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz#90a8b63998b688b284f255c6a5248abd5b28d754"
+  integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==
+
+"@svgr/babel-plugin-transform-svg-component@8.0.0":
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz#013b4bfca88779711f0ed2739f3f7efcefcf4f7e"
+  integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==
+
+"@svgr/babel-preset@8.1.0":
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz#0e87119aecdf1c424840b9d4565b7137cabf9ece"
+  integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==
+  dependencies:
+    "@svgr/babel-plugin-add-jsx-attribute" "8.0.0"
+    "@svgr/babel-plugin-remove-jsx-attribute" "8.0.0"
+    "@svgr/babel-plugin-remove-jsx-empty-expression" "8.0.0"
+    "@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0"
+    "@svgr/babel-plugin-svg-dynamic-title" "8.0.0"
+    "@svgr/babel-plugin-svg-em-dimensions" "8.0.0"
+    "@svgr/babel-plugin-transform-react-native-svg" "8.1.0"
+    "@svgr/babel-plugin-transform-svg-component" "8.0.0"
+
+"@svgr/core@^8.1.0":
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/@svgr/core/-/core-8.1.0.tgz#41146f9b40b1a10beaf5cc4f361a16a3c1885e88"
+  integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==
+  dependencies:
+    "@babel/core" "^7.21.3"
+    "@svgr/babel-preset" "8.1.0"
+    camelcase "^6.2.0"
+    cosmiconfig "^8.1.3"
+    snake-case "^3.0.4"
+
+"@svgr/hast-util-to-babel-ast@8.0.0":
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz#6952fd9ce0f470e1aded293b792a2705faf4ffd4"
+  integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==
+  dependencies:
+    "@babel/types" "^7.21.3"
+    entities "^4.4.0"
+
+"@svgr/plugin-jsx@^8.1.0":
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz#96969f04a24b58b174ee4cd974c60475acbd6928"
+  integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==
+  dependencies:
+    "@babel/core" "^7.21.3"
+    "@svgr/babel-preset" "8.1.0"
+    "@svgr/hast-util-to-babel-ast" "8.0.0"
+    svg-parser "^2.0.4"
+
+"@swc/core-darwin-arm64@1.6.7":
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.7.tgz#e98a0da9635297728a97faf7f4e11c46f8dfbb46"
+  integrity sha512-sNb+ghP2OhZyUjS7E5Mf3PqSvoXJ5gY6GBaH2qp8WQxx9VL7ozC4HVo6vkeFJBN5cmYqUCLnhrM3HU4W+7yMSA==
+
+"@swc/core-darwin-x64@1.6.7":
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.6.7.tgz#fccd389046a8fe0d8b294f9657b3861046fcd3bb"
+  integrity sha512-LQwYm/ATYN5fYSYVPMfComPiFo5i8jh75h1ASvNWhXtS+/+k1dq1zXTJWZRuojd5NXgW3bb6mJtJ2evwYIgYbA==
+
+"@swc/core-linux-arm-gnueabihf@1.6.7":
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.7.tgz#f384235e5f14870646157017eb06dfbaed0894c0"
+  integrity sha512-kEDzVhNci38LX3kdY99t68P2CDf+2QFDk5LawVamXH0iN5DRAO/+wjOhxL8KOHa6wQVqKEt5WrhD+Rrvk/34Yw==
+
+"@swc/core-linux-arm64-gnu@1.6.7":
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.7.tgz#d2b8c0c6045eecb96bc3f3dfa7fb31b5ab708cdf"
+  integrity sha512-SyOBUGfl31xLGpIJ/Jd6GKHtkfZyHBXSwFlK7FmPN//MBQLtTBm4ZaWTnWnGo4aRsJwQdXWDKPyqlMBtnIl1nQ==
+
+"@swc/core-linux-arm64-musl@1.6.7":
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.7.tgz#6ae2a160ba535b1f4747d35a124f410545092abe"
+  integrity sha512-1fOAXkDFbRfItEdMZPxT3du1QWYhgToa4YsnqTujjE8EqJW8K27hIcHRIkVuzp7PNhq8nLBg0JpJM4g27EWD7g==
+
+"@swc/core-linux-x64-gnu@1.6.7":
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.7.tgz#6ebcf76fa868321c3b079e5c668c137b9b91df49"
+  integrity sha512-Gp7uCwPsNO5ATxbyvfTyeNCHUGD9oA+xKMm43G1tWCy+l07gLqWMKp7DIr3L3qPD05TfAVo3OuiOn2abpzOFbw==
+
+"@swc/core-linux-x64-musl@1.6.7":
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.7.tgz#41531ef3e1c7123d87b7a7a1b984fa2689032621"
+  integrity sha512-QeruGBZJ15tadqEMQ77ixT/CYGk20MtlS8wmvJiV+Wsb8gPW5LgCjtupzcLLnoQzDG54JGNCeeZ0l/T8NYsOvA==
+
+"@swc/core-win32-arm64-msvc@1.6.7":
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.7.tgz#af0b84a54d01bc3aad12acffa98ebb13fc03c3e6"
+  integrity sha512-ouRqgSnT95lTCiU/6kJRNS5b1o+p8I/V9jxtL21WUj/JOVhsFmBErqQ0MZyCu514noWiR5BIqOrZXR8C1Knx6Q==
+
+"@swc/core-win32-ia32-msvc@1.6.7":
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.7.tgz#c454851c05c26f67d2edc399e1cde9d074744ce4"
+  integrity sha512-eZAP/EmJ0IcfgAx6B4/SpSjq3aT8gr0ooktfMqw/w0/5lnNrbMl2v+2kvxcneNcF7bp8VNcYZnoHlsP+LvmVbA==
+
+"@swc/core-win32-x64-msvc@1.6.7":
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.7.tgz#6ee4a3caf3466971e6b5fb2fba4674924507a2de"
+  integrity sha512-QOdE+7GQg1UQPS6p0KxzJOh/8GLbJ5zI1vqKArCCB0unFqUfKIjYb2TaH0geEBy3w9qtXxe3ZW6hzxtZSS9lDg==
 
 "@swc/core@^1.5.7":
-  version "1.5.27"
-  resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.5.27.tgz#a3c44addc8f549010373a168b408b84078463375"
-  integrity sha512-HmSSCBoUSRDFAd8aEB+WILkCofIp1c2OU6ZJWu1aCt6pijwQSkA4y51CTBcdvyy/+zX1W3cic7alfdhmQxxeEQ==
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.6.7.tgz#5d113df161fd8ec29ab8837f385240f41315735e"
+  integrity sha512-BBzORL9qWz5hZqAZ83yn+WNaD54RH5eludjqIOboolFOK/Pw+2l00/H77H4CEBJnzCIBQszsyqtITmrn4evp0g==
   dependencies:
     "@swc/counter" "^0.1.3"
-    "@swc/types" "^0.1.8"
+    "@swc/types" "^0.1.9"
   optionalDependencies:
-    "@swc/core-darwin-arm64" "1.5.27"
-    "@swc/core-darwin-x64" "1.5.27"
-    "@swc/core-linux-arm-gnueabihf" "1.5.27"
-    "@swc/core-linux-arm64-gnu" "1.5.27"
-    "@swc/core-linux-arm64-musl" "1.5.27"
-    "@swc/core-linux-x64-gnu" "1.5.27"
-    "@swc/core-linux-x64-musl" "1.5.27"
-    "@swc/core-win32-arm64-msvc" "1.5.27"
-    "@swc/core-win32-ia32-msvc" "1.5.27"
-    "@swc/core-win32-x64-msvc" "1.5.27"
+    "@swc/core-darwin-arm64" "1.6.7"
+    "@swc/core-darwin-x64" "1.6.7"
+    "@swc/core-linux-arm-gnueabihf" "1.6.7"
+    "@swc/core-linux-arm64-gnu" "1.6.7"
+    "@swc/core-linux-arm64-musl" "1.6.7"
+    "@swc/core-linux-x64-gnu" "1.6.7"
+    "@swc/core-linux-x64-musl" "1.6.7"
+    "@swc/core-win32-arm64-msvc" "1.6.7"
+    "@swc/core-win32-ia32-msvc" "1.6.7"
+    "@swc/core-win32-x64-msvc" "1.6.7"
 
 "@swc/counter@^0.1.3":
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
   integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
 
-"@swc/types@^0.1.8":
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.8.tgz#2c81d107c86cfbd0c3a05ecf7bb54c50dfa58a95"
-  integrity sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA==
+"@swc/types@^0.1.9":
+  version "0.1.9"
+  resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.9.tgz#e67cdcc2e4dd74a3cef4474b465eb398e7ae83e2"
+  integrity sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==
   dependencies:
     "@swc/counter" "^0.1.3"
 
-"@types/estree@1.0.5":
+"@types/estree@1.0.5", "@types/estree@^1.0.0":
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
   integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
@@ -817,61 +986,61 @@
     csstype "^3.0.2"
 
 "@typescript-eslint/eslint-plugin@^7.2.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.13.0.tgz#3cdeb5d44d051b21a9567535dd90702b2a42c6ff"
-  integrity sha512-FX1X6AF0w8MdVFLSdqwqN/me2hyhuQg4ykN6ZpVhh1ij/80pTvDKclX1sZB9iqex8SjQfVhwMKs3JtnnMLzG9w==
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz#8eaf396ac2992d2b8f874b68eb3fcd6b179cb7f3"
+  integrity sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==
   dependencies:
     "@eslint-community/regexpp" "^4.10.0"
-    "@typescript-eslint/scope-manager" "7.13.0"
-    "@typescript-eslint/type-utils" "7.13.0"
-    "@typescript-eslint/utils" "7.13.0"
-    "@typescript-eslint/visitor-keys" "7.13.0"
+    "@typescript-eslint/scope-manager" "7.15.0"
+    "@typescript-eslint/type-utils" "7.15.0"
+    "@typescript-eslint/utils" "7.15.0"
+    "@typescript-eslint/visitor-keys" "7.15.0"
     graphemer "^1.4.0"
     ignore "^5.3.1"
     natural-compare "^1.4.0"
     ts-api-utils "^1.3.0"
 
 "@typescript-eslint/parser@^7.2.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.13.0.tgz#9489098d68d57ad392f507495f2b82ce8b8f0a6b"
-  integrity sha512-EjMfl69KOS9awXXe83iRN7oIEXy9yYdqWfqdrFAYAAr6syP8eLEFI7ZE4939antx2mNgPRW/o1ybm2SFYkbTVA==
-  dependencies:
-    "@typescript-eslint/scope-manager" "7.13.0"
-    "@typescript-eslint/types" "7.13.0"
-    "@typescript-eslint/typescript-estree" "7.13.0"
-    "@typescript-eslint/visitor-keys" "7.13.0"
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.15.0.tgz#f4a536e5fc6a1c05c82c4d263a2bfad2da235c80"
+  integrity sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==
+  dependencies:
+    "@typescript-eslint/scope-manager" "7.15.0"
+    "@typescript-eslint/types" "7.15.0"
+    "@typescript-eslint/typescript-estree" "7.15.0"
+    "@typescript-eslint/visitor-keys" "7.15.0"
     debug "^4.3.4"
 
-"@typescript-eslint/scope-manager@7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.13.0.tgz#6927d6451537ce648c6af67a2327378d4cc18462"
-  integrity sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==
+"@typescript-eslint/scope-manager@7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz#201b34b0720be8b1447df17b963941bf044999b2"
+  integrity sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==
   dependencies:
-    "@typescript-eslint/types" "7.13.0"
-    "@typescript-eslint/visitor-keys" "7.13.0"
+    "@typescript-eslint/types" "7.15.0"
+    "@typescript-eslint/visitor-keys" "7.15.0"
 
-"@typescript-eslint/type-utils@7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.13.0.tgz#4587282b5227a23753ea8b233805ecafc3924c76"
-  integrity sha512-xMEtMzxq9eRkZy48XuxlBFzpVMDurUAfDu5Rz16GouAtXm0TaAoTFzqWUFPPuQYXI/CDaH/Bgx/fk/84t/Bc9A==
+"@typescript-eslint/type-utils@7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz#5b83c904c6de91802fb399305a50a56d10472c39"
+  integrity sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==
   dependencies:
-    "@typescript-eslint/typescript-estree" "7.13.0"
-    "@typescript-eslint/utils" "7.13.0"
+    "@typescript-eslint/typescript-estree" "7.15.0"
+    "@typescript-eslint/utils" "7.15.0"
     debug "^4.3.4"
     ts-api-utils "^1.3.0"
 
-"@typescript-eslint/types@7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.13.0.tgz#0cca95edf1f1fdb0cfe1bb875e121b49617477c5"
-  integrity sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==
+"@typescript-eslint/types@7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.15.0.tgz#fb894373a6e3882cbb37671ffddce44f934f62fc"
+  integrity sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==
 
-"@typescript-eslint/typescript-estree@7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.13.0.tgz#4cc24fc155088ebf3b3adbad62c7e60f72c6de1c"
-  integrity sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==
+"@typescript-eslint/typescript-estree@7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz#e323bfa3966e1485b638ce751f219fc1f31eba37"
+  integrity sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==
   dependencies:
-    "@typescript-eslint/types" "7.13.0"
-    "@typescript-eslint/visitor-keys" "7.13.0"
+    "@typescript-eslint/types" "7.15.0"
+    "@typescript-eslint/visitor-keys" "7.15.0"
     debug "^4.3.4"
     globby "^11.1.0"
     is-glob "^4.0.3"
@@ -879,22 +1048,22 @@
     semver "^7.6.0"
     ts-api-utils "^1.3.0"
 
-"@typescript-eslint/utils@7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.13.0.tgz#f84e7e8aeceae945a9a3f40d077fd95915308004"
-  integrity sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==
+"@typescript-eslint/utils@7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.15.0.tgz#9e6253c4599b6e7da2fb64ba3f549c73eb8c1960"
+  integrity sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==
   dependencies:
     "@eslint-community/eslint-utils" "^4.4.0"
-    "@typescript-eslint/scope-manager" "7.13.0"
-    "@typescript-eslint/types" "7.13.0"
-    "@typescript-eslint/typescript-estree" "7.13.0"
+    "@typescript-eslint/scope-manager" "7.15.0"
+    "@typescript-eslint/types" "7.15.0"
+    "@typescript-eslint/typescript-estree" "7.15.0"
 
-"@typescript-eslint/visitor-keys@7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.13.0.tgz#2eb7ce8eb38c2b0d4a494d1fe1908e7071a1a353"
-  integrity sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==
+"@typescript-eslint/visitor-keys@7.15.0":
+  version "7.15.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz#1da0726201a859343fe6a05742a7c1792fff5b66"
+  integrity sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==
   dependencies:
-    "@typescript-eslint/types" "7.13.0"
+    "@typescript-eslint/types" "7.15.0"
     eslint-visitor-keys "^3.4.3"
 
 "@ungap/structured-clone@^1.2.0":
@@ -909,15 +1078,20 @@
   dependencies:
     "@swc/core" "^1.5.7"
 
+"@yr/monotone-cubic-spline@^1.0.3":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz#7272d89f8e4f6fb7a1600c28c378cc18d3b577b9"
+  integrity sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==
+
 acorn-jsx@^5.3.2:
   version "5.3.2"
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
 
 acorn@^8.9.0:
-  version "8.11.3"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
-  integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
+  version "8.12.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
+  integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
 
 ajv@^6.12.4:
   version "6.12.6"
@@ -948,6 +1122,19 @@ ansi-styles@^4.1.0:
   dependencies:
     color-convert "^2.0.1"
 
+apexcharts@^3.50.0:
+  version "3.50.0"
+  resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.50.0.tgz#9030f206183978df0d762602b3d15b59ec6e783b"
+  integrity sha512-LJT1PNAm+NoIU3aogL2P+ViC0y/Cjik54FdzzGV54UNnGQLBoLe5ok3fxsJDTgyez45BGYT8gqNpYKqhdfy5sg==
+  dependencies:
+    "@yr/monotone-cubic-spline" "^1.0.3"
+    svg.draggable.js "^2.2.2"
+    svg.easing.js "^2.0.0"
+    svg.filter.js "^2.0.2"
+    svg.pathmorphing.js "^0.1.3"
+    svg.resize.js "^1.4.3"
+    svg.select.js "^3.0.1"
+
 argparse@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
@@ -994,11 +1181,31 @@ braces@^3.0.3:
   dependencies:
     fill-range "^7.1.1"
 
+browserslist@^4.22.2:
+  version "4.23.1"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96"
+  integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==
+  dependencies:
+    caniuse-lite "^1.0.30001629"
+    electron-to-chromium "^1.4.796"
+    node-releases "^2.0.14"
+    update-browserslist-db "^1.0.16"
+
 callsites@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 
+camelcase@^6.2.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
+  integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
+
+caniuse-lite@^1.0.30001629:
+  version "1.0.30001640"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz#32c467d4bf1f1a0faa63fc793c2ba81169e7652f"
+  integrity sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==
+
 chalk@^2.4.2:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@@ -1016,6 +1223,16 @@ chalk@^4.0.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
+classnames@^2.3.0:
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
+  integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
+
+clsx@^1.1.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
+  integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+
 clsx@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
@@ -1055,6 +1272,11 @@ convert-source-map@^1.5.0:
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
   integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
 
+convert-source-map@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
+  integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
+
 cosmiconfig@^7.0.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
@@ -1066,6 +1288,16 @@ cosmiconfig@^7.0.0:
     path-type "^4.0.0"
     yaml "^1.10.0"
 
+cosmiconfig@^8.1.3:
+  version "8.3.6"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3"
+  integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==
+  dependencies:
+    import-fresh "^3.3.0"
+    js-yaml "^4.1.0"
+    parse-json "^5.2.0"
+    path-type "^4.0.0"
+
 cross-spawn@^7.0.2:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -1080,7 +1312,7 @@ csstype@^3.0.2, csstype@^3.1.3:
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
   integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
 
-debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
+debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
   version "4.3.5"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e"
   integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==
@@ -1114,6 +1346,24 @@ dom-helpers@^5.0.1:
     "@babel/runtime" "^7.8.7"
     csstype "^3.0.2"
 
+dot-case@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
+  integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
+  dependencies:
+    no-case "^3.0.4"
+    tslib "^2.0.3"
+
+electron-to-chromium@^1.4.796:
+  version "1.4.818"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.818.tgz#7762c8bfd15a07c3833b7f5deed990e9e5a4c24f"
+  integrity sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==
+
+entities@^4.4.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
+  integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+
 error-ex@^1.3.1:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@@ -1121,34 +1371,39 @@ error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-esbuild@^0.20.1:
-  version "0.20.2"
-  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1"
-  integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==
+esbuild@^0.21.3:
+  version "0.21.5"
+  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
+  integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
   optionalDependencies:
-    "@esbuild/aix-ppc64" "0.20.2"
-    "@esbuild/android-arm" "0.20.2"
-    "@esbuild/android-arm64" "0.20.2"
-    "@esbuild/android-x64" "0.20.2"
-    "@esbuild/darwin-arm64" "0.20.2"
-    "@esbuild/darwin-x64" "0.20.2"
-    "@esbuild/freebsd-arm64" "0.20.2"
-    "@esbuild/freebsd-x64" "0.20.2"
-    "@esbuild/linux-arm" "0.20.2"
-    "@esbuild/linux-arm64" "0.20.2"
-    "@esbuild/linux-ia32" "0.20.2"
-    "@esbuild/linux-loong64" "0.20.2"
-    "@esbuild/linux-mips64el" "0.20.2"
-    "@esbuild/linux-ppc64" "0.20.2"
-    "@esbuild/linux-riscv64" "0.20.2"
-    "@esbuild/linux-s390x" "0.20.2"
-    "@esbuild/linux-x64" "0.20.2"
-    "@esbuild/netbsd-x64" "0.20.2"
-    "@esbuild/openbsd-x64" "0.20.2"
-    "@esbuild/sunos-x64" "0.20.2"
-    "@esbuild/win32-arm64" "0.20.2"
-    "@esbuild/win32-ia32" "0.20.2"
-    "@esbuild/win32-x64" "0.20.2"
+    "@esbuild/aix-ppc64" "0.21.5"
+    "@esbuild/android-arm" "0.21.5"
+    "@esbuild/android-arm64" "0.21.5"
+    "@esbuild/android-x64" "0.21.5"
+    "@esbuild/darwin-arm64" "0.21.5"
+    "@esbuild/darwin-x64" "0.21.5"
+    "@esbuild/freebsd-arm64" "0.21.5"
+    "@esbuild/freebsd-x64" "0.21.5"
+    "@esbuild/linux-arm" "0.21.5"
+    "@esbuild/linux-arm64" "0.21.5"
+    "@esbuild/linux-ia32" "0.21.5"
+    "@esbuild/linux-loong64" "0.21.5"
+    "@esbuild/linux-mips64el" "0.21.5"
+    "@esbuild/linux-ppc64" "0.21.5"
+    "@esbuild/linux-riscv64" "0.21.5"
+    "@esbuild/linux-s390x" "0.21.5"
+    "@esbuild/linux-x64" "0.21.5"
+    "@esbuild/netbsd-x64" "0.21.5"
+    "@esbuild/openbsd-x64" "0.21.5"
+    "@esbuild/sunos-x64" "0.21.5"
+    "@esbuild/win32-arm64" "0.21.5"
+    "@esbuild/win32-ia32" "0.21.5"
+    "@esbuild/win32-x64" "0.21.5"
+
+escalade@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
+  integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
 
 escape-string-regexp@^1.0.5:
   version "1.0.5"
@@ -1255,6 +1510,11 @@ estraverse@^5.1.0, estraverse@^5.2.0:
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
   integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
 
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
 esutils@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
@@ -1349,6 +1609,11 @@ function-bind@^1.1.2:
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
   integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
 
+gensync@^1.0.0-beta.2:
+  version "1.0.0-beta.2"
+  resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
+  integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+
 glob-parent@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -1399,6 +1664,11 @@ globby@^11.1.0:
     merge2 "^1.4.1"
     slash "^3.0.0"
 
+goober@^2.0.33:
+  version "2.1.14"
+  resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.14.tgz#4a5c94fc34dc086a8e6035360ae1800005135acd"
+  integrity sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==
+
 graphemer@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
@@ -1414,7 +1684,7 @@ has-flag@^4.0.0:
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
-hasown@^2.0.0:
+hasown@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
   integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
@@ -1433,7 +1703,7 @@ ignore@^5.2.0, ignore@^5.3.1:
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
   integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
 
-import-fresh@^3.2.1:
+import-fresh@^3.2.1, import-fresh@^3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
   integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@@ -1465,11 +1735,11 @@ is-arrayish@^0.2.1:
   integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
 
 is-core-module@^2.13.0:
-  version "2.13.1"
-  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
-  integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
+  version "2.14.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.14.0.tgz#43b8ef9f46a6a08888db67b1ffd4ec9e3dfd59d1"
+  integrity sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==
   dependencies:
-    hasown "^2.0.0"
+    hasown "^2.0.2"
 
 is-extglob@^2.1.1:
   version "2.1.1"
@@ -1540,6 +1810,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
   resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
   integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
 
+json5@^2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+  integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
 keyv@^4.5.3:
   version "4.5.4"
   resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -1579,6 +1854,25 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
   dependencies:
     js-tokens "^3.0.0 || ^4.0.0"
 
+lower-case@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
+  integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
+  dependencies:
+    tslib "^2.0.3"
+
+lru-cache@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+  integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+  dependencies:
+    yallist "^3.0.2"
+
+material-ui-confirm@^3.0.16:
+  version "3.0.16"
+  resolved "https://registry.yarnpkg.com/material-ui-confirm/-/material-ui-confirm-3.0.16.tgz#8b25b4770a0f15d888c838bcd21180f655e03469"
+  integrity sha512-aJoa/FM/U/86qztoljlk8FWmjSJbAMzDWCdWbDqU5WwB0WzcWPyGrhBvIqihR9uKdHKBf1YrvMjn68uOrfsXAg==
+
 mdi-material-ui@^7.9.1:
   version "7.9.1"
   resolved "https://registry.yarnpkg.com/mdi-material-ui/-/mdi-material-ui-7.9.1.tgz#f28dbd6883a8c6198ca78e19c9f23728b2599226"
@@ -1605,9 +1899,9 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
     brace-expansion "^1.1.7"
 
 minimatch@^9.0.4:
-  version "9.0.4"
-  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51"
-  integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==
+  version "9.0.5"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
+  integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
   dependencies:
     brace-expansion "^2.0.1"
 
@@ -1626,6 +1920,27 @@ natural-compare@^1.4.0:
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
   integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
 
+no-case@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
+  integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
+  dependencies:
+    lower-case "^2.0.2"
+    tslib "^2.0.3"
+
+node-releases@^2.0.14:
+  version "2.0.14"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
+  integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
+
+notistack@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/notistack/-/notistack-3.0.1.tgz#daf59888ab7e2c30a1fa8f71f9cba2978773236e"
+  integrity sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==
+  dependencies:
+    clsx "^1.1.0"
+    goober "^2.0.33"
+
 object-assign@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -1671,7 +1986,7 @@ parent-module@^1.0.0:
   dependencies:
     callsites "^3.0.0"
 
-parse-json@^5.0.0:
+parse-json@^5.0.0, parse-json@^5.2.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
   integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
@@ -1706,7 +2021,7 @@ path-type@^4.0.0:
   resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
   integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
 
-picocolors@^1.0.0:
+picocolors@^1.0.0, picocolors@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
   integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
@@ -1716,13 +2031,13 @@ picomatch@^2.3.1:
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
   integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
 
-postcss@^8.4.38:
-  version "8.4.38"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
-  integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
+postcss@^8.4.39:
+  version "8.4.39"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3"
+  integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==
   dependencies:
     nanoid "^3.3.7"
-    picocolors "^1.0.0"
+    picocolors "^1.0.1"
     source-map-js "^1.2.0"
 
 prelude-ls@^1.2.1:
@@ -1749,6 +2064,20 @@ queue-microtask@^1.2.2:
   resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
   integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
 
+react-apexcharts@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/react-apexcharts/-/react-apexcharts-1.4.1.tgz#95ab31e4d2201308f59f3d2a4b65d10d9d0ea4bb"
+  integrity sha512-G14nVaD64Bnbgy8tYxkjuXEUp/7h30Q0U33xc3AwtGFijJB9nHqOt1a6eG0WBn055RgRg+NwqbKGtqPxy15d0Q==
+  dependencies:
+    prop-types "^15.8.1"
+
+react-device-detect@^2.2.3:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-2.2.3.tgz#97a7ae767cdd004e7c3578260f48cf70c036e7ca"
+  integrity sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==
+  dependencies:
+    ua-parser-js "^1.0.33"
+
 react-dom@^18.2.0:
   version "18.3.1"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
@@ -1768,12 +2097,12 @@ react-is@^18.2.0:
   integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
 
 react-router-dom@^6.23.1:
-  version "6.23.1"
-  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.23.1.tgz#30cbf266669693e9492aa4fc0dde2541ab02322f"
-  integrity sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.24.1.tgz#b1a22f7d6c5a1bfce30732bd370713f991ab4de4"
+  integrity sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==
   dependencies:
-    "@remix-run/router" "1.16.1"
-    react-router "6.23.1"
+    "@remix-run/router" "1.17.1"
+    react-router "6.24.1"
 
 react-router-hash-link@^2.4.3:
   version "2.4.3"
@@ -1782,12 +2111,20 @@ react-router-hash-link@^2.4.3:
   dependencies:
     prop-types "^15.7.2"
 
-react-router@6.23.1:
-  version "6.23.1"
-  resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.23.1.tgz#d08cbdbd9d6aedc13eea6e94bc6d9b29cb1c4be9"
-  integrity sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==
+react-router@6.24.1:
+  version "6.24.1"
+  resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.24.1.tgz#5a3bbba0000afba68d42915456ca4c806f37a7de"
+  integrity sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==
   dependencies:
-    "@remix-run/router" "1.16.1"
+    "@remix-run/router" "1.17.1"
+
+react-tooltip@^5.27.0:
+  version "5.27.1"
+  resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.27.1.tgz#a94481ba146d828d31642f14d6ab29b56998fcda"
+  integrity sha512-a+micPXcMOMt11CYlwJD4XShcqGziasHco4NPe1OFw298WBTILMyzUgNC1LAFViAe791JdHNVSJIpzhZm2MvDA==
+  dependencies:
+    "@floating-ui/dom" "^1.6.1"
+    classnames "^2.3.0"
 
 react-transition-group@^4.4.5:
   version "4.4.5"
@@ -1876,6 +2213,11 @@ scheduler@^0.23.2:
   dependencies:
     loose-envify "^1.1.0"
 
+semver@^6.3.1:
+  version "6.3.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
+  integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
+
 semver@^7.6.0:
   version "7.6.2"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
@@ -1898,6 +2240,14 @@ slash@^3.0.0:
   resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
   integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
 
+snake-case@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
+  integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==
+  dependencies:
+    dot-case "^3.0.4"
+    tslib "^2.0.3"
+
 source-map-js@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
@@ -1944,6 +2294,66 @@ supports-preserve-symlinks-flag@^1.0.0:
   resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 
+svg-parser@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
+  integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
+
+svg.draggable.js@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz#c514a2f1405efb6f0263e7958f5b68fce50603ba"
+  integrity sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==
+  dependencies:
+    svg.js "^2.0.1"
+
+svg.easing.js@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/svg.easing.js/-/svg.easing.js-2.0.0.tgz#8aa9946b0a8e27857a5c40a10eba4091e5691f12"
+  integrity sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==
+  dependencies:
+    svg.js ">=2.3.x"
+
+svg.filter.js@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/svg.filter.js/-/svg.filter.js-2.0.2.tgz#91008e151389dd9230779fcbe6e2c9a362d1c203"
+  integrity sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==
+  dependencies:
+    svg.js "^2.2.5"
+
+svg.js@>=2.3.x, svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/svg.js/-/svg.js-2.7.1.tgz#eb977ed4737001eab859949b4a398ee1bb79948d"
+  integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==
+
+svg.pathmorphing.js@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz#c25718a1cc7c36e852ecabc380e758ac09bb2b65"
+  integrity sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==
+  dependencies:
+    svg.js "^2.4.0"
+
+svg.resize.js@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/svg.resize.js/-/svg.resize.js-1.4.3.tgz#885abd248e0cd205b36b973c4b578b9a36f23332"
+  integrity sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==
+  dependencies:
+    svg.js "^2.6.5"
+    svg.select.js "^2.1.2"
+
+svg.select.js@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-2.1.2.tgz#e41ce13b1acff43a7441f9f8be87a2319c87be73"
+  integrity sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==
+  dependencies:
+    svg.js "^2.2.5"
+
+svg.select.js@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-3.0.1.tgz#a4198e359f3825739226415f82176a90ea5cc917"
+  integrity sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==
+  dependencies:
+    svg.js "^2.6.5"
+
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -1966,6 +2376,11 @@ ts-api-utils@^1.3.0:
   resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
   integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
 
+tslib@^2.0.3:
+  version "2.6.3"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
+  integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
+
 type-check@^0.4.0, type-check@~0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@@ -1979,9 +2394,22 @@ type-fest@^0.20.2:
   integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
 
 typescript@^5.2.2:
-  version "5.4.5"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
-  integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
+  version "5.5.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa"
+  integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==
+
+ua-parser-js@^1.0.33:
+  version "1.0.38"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.38.tgz#66bb0c4c0e322fe48edfe6d446df6042e62f25e2"
+  integrity sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==
+
+update-browserslist-db@^1.0.16:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e"
+  integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==
+  dependencies:
+    escalade "^3.1.2"
+    picocolors "^1.0.1"
 
 uri-js@^4.2.2:
   version "4.4.1"
@@ -1990,13 +2418,22 @@ uri-js@^4.2.2:
   dependencies:
     punycode "^2.1.0"
 
+vite-plugin-svgr@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz#9f3bf5206b0ec510287e56d16f1915e729bb4e6b"
+  integrity sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==
+  dependencies:
+    "@rollup/pluginutils" "^5.0.5"
+    "@svgr/core" "^8.1.0"
+    "@svgr/plugin-jsx" "^8.1.0"
+
 vite@^5.2.0:
-  version "5.2.13"
-  resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.13.tgz#945ababcbe3d837ae2479c29f661cd20bc5e1a80"
-  integrity sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==
+  version "5.3.3"
+  resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.3.tgz#5265b1f0a825b3b6564c2d07524777c83e3c04c2"
+  integrity sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==
   dependencies:
-    esbuild "^0.20.1"
-    postcss "^8.4.38"
+    esbuild "^0.21.3"
+    postcss "^8.4.39"
     rollup "^4.13.0"
   optionalDependencies:
     fsevents "~2.3.3"
@@ -2018,6 +2455,11 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 
+yallist@^3.0.2:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+  integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
 yaml@^1.10.0:
   version "1.10.2"
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"