diff --git a/Justfile b/Justfile index 0d99e3a..432fbd1 100644 --- a/Justfile +++ b/Justfile @@ -11,7 +11,7 @@ start-release: build-container-release remove-podman-containers # Removes and stops any containers related to the project [private] remove-podman-containers: - podman container rm -f ttime + podman container rm -fi ttime # Saves the release container to a tarball, pigz is just gzip but multithreaded save-release: build-container-release @@ -23,14 +23,14 @@ load-release file: # Tests every part of the project testall: - cd backend && make test - cd backend && make lint cd frontend && npm test cd frontend && npm run lint + cd backend && make test + cd backend && make lint # Cleans up everything related to the project clean: remove-podman-containers - podman image rm -f ttime-server + podman image rm -fi ttime-server rm -rf frontend/dist rm -rf frontend/node_modules rm -f ttime-server.tar.gz @@ -41,3 +41,6 @@ clean: remove-podman-containers [confirm] podman-clean: podman system reset --force + +install-linter: + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.56.2 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..668ccf1 --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +# Builds a release container +build-container-release: + podman build -t ttime-server -f container/Containerfile . + +# Builds a release container and runs it +start-release: build-container-release remove-podman-containers + podman run -d -e DATABASE_URL=sqlite:release.db -p 8080:8080 --name ttime ttime-server + @echo "Started production ttime-server on http://localhost:8080" + +# Removes and stops any containers related to the project +remove-podman-containers: + podman container rm -fi ttime + +# Tests every part of the project +testall: + cd frontend && npm test + cd frontend && npm run lint + cd backend && make test + cd backend && make lint + +# Cleans up everything related to the project +clean: remove-podman-containers + podman image rm -fi ttime-server + rm -rf frontend/dist + rm -rf frontend/node_modules + rm -f ttime-server.tar.gz + cd backend && make clean + @echo "Cleaned up!" + +# Cleans up everything related to podman, not just the project. Make sure you understand what this means. +podman-clean: + podman system reset --force + +# Installs the linter, which is not included in the ubuntu repo +install-linter: + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.56.2 + +# This installs just, a make alternative, which is slightly more ergonomic to use +install-just: + @echo "Installing just" + @curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin \ No newline at end of file diff --git a/README.md b/README.md index fc55126..d75861a 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ My recommendation would be to make WSL your primary development environment if y You should consult the [WSL documentation](https://docs.microsoft.com/en-us/windows/wsl/install), but for any recent version of windows, installation essentially boils down to running the following command in **PowerShell as an administrator**: ```powershell -wsl --install +wsl --install -d Ubuntu-22.04 # To get a somewhat recent version of Go ``` If you get any errors related to virtualization, you will need to enable virtualization in the BIOS. This is a common issue, and you can find a guide for your specific motherboard online. This is a one-time operation and will not affect your windows installation. This setting is usually called "VT-x" or "AMD-V" and is usually found in the CPU settings. If you can't find it, shoot me a message and I'll find it for you. diff --git a/backend/Makefile b/backend/Makefile index 23eefa0..dcc79b4 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -69,3 +69,7 @@ lint: # Default target default: build + +install-just: + @echo "Installing just" + @curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 5762bab..bae7a83 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -9,6 +9,8 @@ import ( "github.com/gofiber/fiber/v2" _ "github.com/mattn/go-sqlite3" + + jwtware "github.com/gofiber/contrib/jwt" ) func main() { @@ -33,9 +35,23 @@ func main() { // This will likely be replaced by an embedded filesystem in the future server.Static("/", "./static") - // Register our handlers + // Register our unprotected routes server.Post("/api/register", gs.Register) + // Register handlers for example button count + server.Get("/api/button", gs.GetButtonCount) + server.Post("/api/button", gs.IncrementButtonCount) + + server.Post("/api/login", gs.Login) + + // Every route from here on will require a valid JWT + server.Use(jwtware.New(jwtware.Config{ + SigningKey: jwtware.SigningKey{Key: []byte("secret")}, + })) + + server.Post("/api/loginrenew", gs.LoginRenew) + server.Delete("/api/userdelete", gs.UserDelete) // Perhaps just use POST to avoid headaches + // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) if err != nil { diff --git a/backend/go.mod b/backend/go.mod index fdbbaf7..94d5519 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -8,18 +8,24 @@ require ( github.com/mattn/go-sqlite3 v1.14.22 ) +require ( + github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect + github.com/gofiber/contrib/jwt v1.0.8 + github.com/golang-jwt/jwt/v5 v5.2.1 +) + // These are all for fiber require ( - github.com/andybalholm/brotli v1.0.5 // indirect - github.com/gofiber/fiber/v2 v2.52.1 // indirect - github.com/google/uuid v1.5.0 // indirect - github.com/klauspost/compress v1.17.0 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/gofiber/fiber/v2 v2.52.2 + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.7 // 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.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/fasthttp v1.52.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.18.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 9ddb540..42908f6 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,17 +1,31 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= +github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gofiber/contrib/jwt v1.0.8 h1:/GeOsm/Mr1OGr0GTy+RIVSz5VgNNyP3ZgK4wdqxF/WY= +github.com/gofiber/contrib/jwt v1.0.8/go.mod h1:gWWBtBiLmKXRN7xy6a96QO0KGvPEyxdh8x496Ujtg84= github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI= github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo= +github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -26,13 +40,19 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 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/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.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= +github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= 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/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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 6e86641..84937f1 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -14,9 +14,13 @@ import ( type Database interface { AddUser(username string, password string) error RemoveUser(username string) error + PromoteToAdmin(username string) error GetUserId(username string) (int, error) AddProject(name string, description string, username string) error Migrate(dirname string) error + // AddTimeReport(projectname string, start time.Time, end time.Time) error + // AddUserToProject(username string, projectname string) error + // ChangeUserRole(username string, projectname string, role string) error } // This struct is a wrapper type that holds the database connection @@ -30,6 +34,11 @@ var scripts embed.FS const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" const projectInsert = "INSERT INTO projects (name, description, user_id) SELECT ?, ?, id FROM users WHERE username = ?" +const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?" + +// const addTimeReport = "" +// const addUserToProject = "" +// const changeUserRole = "" // DbConnect connects to the database func DbConnect(dbpath string) Database { @@ -48,6 +57,18 @@ func DbConnect(dbpath string) Database { return &Db{db} } +// func (d *Db) AddTimeReport(projectname string, start time.Time, end time.Time) error { + +// } + +// func (d *Db) AddUserToProject(username string, projectname string) error { + +// } + +// func (d *Db) ChangeUserRole(username string, projectname string, role string) error { + +// } + // AddUser adds a user to the database func (d *Db) AddUser(username string, password string) error { _, err := d.Exec(userInsert, username, password) @@ -60,6 +81,11 @@ func (d *Db) RemoveUser(username string) error { return err } +func (d *Db) PromoteToAdmin(username string) error { + _, err := d.Exec(promoteToAdmin, username) + return err +} + func (d *Db) GetUserId(username string) (int, error) { var id int err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username) diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 6830668..96eb9b7 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -74,3 +74,32 @@ func TestDbRemoveUser(t *testing.T) { t.Error("RemoveUser failed:", err) } } + +func TestPromoteToAdmin(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("test", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.PromoteToAdmin("test") + if err != nil { + t.Error("PromoteToAdmin failed:", err) + } +} + +// func TestAddTimeReport(t *testing.T) { + +// } + +// func TestAddUserToProject(t *testing.T) { + +// } + +// func TestChangeUserRole(t *testing.T) { + +// } diff --git a/backend/internal/database/migrations/0060_site_admin.sql b/backend/internal/database/migrations/0060_site_admin.sql new file mode 100644 index 0000000..14de60d --- /dev/null +++ b/backend/internal/database/migrations/0060_site_admin.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS site_admin ( + admin_id INTEGER PRIMARY KEY, + FOREIGN KEY (admin_id) REFERENCES users (id) ON DELETE CASCADE +) \ No newline at end of file diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 4dae3d8..ffe2072 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -1,25 +1,49 @@ package handlers import ( + "time" "ttime/internal/database" "ttime/internal/types" "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" ) // The actual interface that we will use type GlobalState interface { - Register(c *fiber.Ctx) error + Register(c *fiber.Ctx) error // To register a new user + UserDelete(c *fiber.Ctx) error // To delete a user + Login(c *fiber.Ctx) error // To get the token + LoginRenew(c *fiber.Ctx) error // To renew the token + // CreateProject(c *fiber.Ctx) error // To create a new project + // GetProjects(c *fiber.Ctx) error // To get all projects + // GetProject(c *fiber.Ctx) error // To get a specific project + // UpdateProject(c *fiber.Ctx) error // To update a project + // DeleteProject(c *fiber.Ctx) error // To delete a project + // CreateTask(c *fiber.Ctx) error // To create a new task + // GetTasks(c *fiber.Ctx) error // To get all tasks + // GetTask(c *fiber.Ctx) error // To get a specific task + // UpdateTask(c *fiber.Ctx) error // To update a task + // DeleteTask(c *fiber.Ctx) error // To delete a task + // CreateCollection(c *fiber.Ctx) error // To create a new collection + // GetCollections(c *fiber.Ctx) error // To get all collections + // GetCollection(c *fiber.Ctx) error // To get a specific collection + // UpdateCollection(c *fiber.Ctx) error // To update a collection + // DeleteCollection(c *fiber.Ctx) error // To delete a collection + // SignCollection(c *fiber.Ctx) error // To sign a collection + GetButtonCount(c *fiber.Ctx) error // For demonstration purposes + IncrementButtonCount(c *fiber.Ctx) error // For demonstration purposes } // "Constructor" func NewGlobalState(db database.Database) GlobalState { - return &GState{Db: db} + return &GState{Db: db, ButtonCount: 0} } // The global state, which implements all the handlers type GState struct { - Db database.Database + Db database.Database + ButtonCount int } func (gs *GState) Register(c *fiber.Ctx) error { @@ -34,3 +58,76 @@ func (gs *GState) Register(c *fiber.Ctx) error { return c.Status(200).SendString("User added") } + +// This path should obviously be protected in the future +// UserDelete deletes a user from the database +func (gs *GState) UserDelete(c *fiber.Ctx) error { + u := new(types.User) + if err := c.BodyParser(u); err != nil { + return c.Status(400).SendString(err.Error()) + } + + if err := gs.Db.RemoveUser(u.Username); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("User deleted") +} + +func (gs *GState) GetButtonCount(c *fiber.Ctx) error { + return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) +} + +func (gs *GState) IncrementButtonCount(c *fiber.Ctx) error { + gs.ButtonCount++ + return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) +} + +// Login is a simple login handler that returns a JWT token +func (gs *GState) Login(c *fiber.Ctx) error { + // To test: curl --data "user=user&pass=pass" http://localhost:8080/api/login + user := c.FormValue("user") + pass := c.FormValue("pass") + + // Throws Unauthorized error + if user != "user" || pass != "pass" { + return c.SendStatus(fiber.StatusUnauthorized) + } + + // Create the Claims + claims := jwt.MapClaims{ + "name": user, + "admin": false, + "exp": time.Now().Add(time.Hour * 72).Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Generate encoded token and send it as response. + t, err := token.SignedString([]byte("secret")) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + return c.JSON(fiber.Map{"token": t}) +} + +// LoginRenew is a simple handler that renews the token +func (gs *GState) LoginRenew(c *fiber.Ctx) error { + // For testing: curl localhost:3000/restricted -H "Authorization: Bearer " + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + claims["exp"] = time.Now().Add(time.Hour * 72).Unix() + renewed := jwt.MapClaims{ + "name": claims["name"], + "admin": claims["admin"], + "exp": claims["exp"], + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) + t, err := token.SignedString([]byte("secret")) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.JSON(fiber.Map{"token": t}) +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5131784..eecde70 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,12 @@ "name": "tmp", "version": "0.0.0", "dependencies": { + "localforage": "^1.10.0", + "match-sorter": "^6.3.4", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.2", + "sort-by": "^0.0.2" }, "devDependencies": { "@swc/core": "^1.4.2", @@ -670,6 +674,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", @@ -1782,6 +1797,14 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@remix-run/router": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.2.tgz", + "integrity": "sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz", @@ -4644,6 +4667,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -5898,6 +5926,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -5913,6 +5949,14 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dependencies": { + "lie": "3.1.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5981,6 +6025,15 @@ "tmpl": "1.0.5" } }, + "node_modules/match-sorter": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", + "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6849,6 +6902,36 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-router": { + "version": "6.22.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.2.tgz", + "integrity": "sha512-YD3Dzprzpcq+tBMHBS822tCjnWD3iIZbTeSXMY9LPSG541EfoBGyZ3bS25KEnaZjLcmQpw2AVLkFyfgXY8uvcw==", + "dependencies": { + "@remix-run/router": "1.15.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.2.tgz", + "integrity": "sha512-WgqxD2qySEIBPZ3w0sHH+PUAiamDeszls9tzqMPBDA1YYVucTBXLU7+gtRfcSnhe92A3glPnvSxK2dhNoAVOIQ==", + "dependencies": { + "@remix-run/router": "1.15.2", + "react-router": "6.22.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-shallow-renderer": { "version": "16.15.0", "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", @@ -6924,6 +7007,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -6942,6 +7030,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7242,6 +7335,11 @@ "node": ">=8" } }, + "node_modules/sort-by": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/sort-by/-/sort-by-0.0.2.tgz", + "integrity": "sha512-iOX5oHA4a0eqTMFiWrHYqv924UeRKFBLhym7iwSVG37Egg2wApgZKAjyzM9WZjMwKv6+8Zi+nIaJ7FYsO9EkoA==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9dce126..e8d78c1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,8 +12,12 @@ "test": "jest" }, "dependencies": { + "localforage": "^1.10.0", + "match-sorter": "^6.3.4", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.2", + "sort-by": "^0.0.2" }, "devDependencies": { "@swc/core": "^1.4.2", diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 20e5f1f..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import reactLogo from "./assets/react.svg"; -import viteLogo from "/vite.svg"; -import "./App.css"; -import { CountButton } from "./Components/CountButton"; - -function App(): JSX.Element { - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ); -} - -export default App; diff --git a/frontend/src/Components/TaskList.tsx b/frontend/src/Components/TaskList.tsx new file mode 100644 index 0000000..1e9cb76 --- /dev/null +++ b/frontend/src/Components/TaskList.tsx @@ -0,0 +1,36 @@ +/** + * The shape of a task + */ +interface Task { + id: number; + name: string; +} + +/** + * The props for the TaskList component + */ +interface TaskProps { + tasks: Task[]; +} + +/** + * A simple list of tasks + * @param props - The tasks to display + * @returns {JSX.Element} The task list + * @example + * const tasks = [{ id: 1, name: "Do the thing" }]; + * return ; + */ +export function TaskList(props: TaskProps): JSX.Element { + return ( +
+

Task List

+

Tasks go here

+
    + {props.tasks.map((task) => ( +
  • {task.name}
  • + ))} +
+
+ ); +} diff --git a/frontend/src/Containers/ContainerDemo.tsx b/frontend/src/Containers/ContainerDemo.tsx new file mode 100644 index 0000000..ac1cfc6 --- /dev/null +++ b/frontend/src/Containers/ContainerDemo.tsx @@ -0,0 +1,24 @@ +import { ReactNode } from "react"; + +/** The props for the container */ +interface ContainerProps { + children: ReactNode; +} + +/** + * Contains children + * @param props - The children to contain + * @returns {JSX.Element} The container + */ +export function Container(props: ContainerProps): JSX.Element { + return
{props.children}
; +} + +/** + * Contains even more children + * @param props + * @returns {JSX.Element} + */ +export function Container2(props: ContainerProps): JSX.Element { + return {props.children}; +} diff --git a/frontend/src/Pages/Home.tsx b/frontend/src/Pages/Home.tsx new file mode 100644 index 0000000..7ce73a6 --- /dev/null +++ b/frontend/src/Pages/Home.tsx @@ -0,0 +1,36 @@ +import reactLogo from "../assets/react.svg"; +import viteLogo from "/vite.svg"; +import "../index.css"; +import { CountButton } from "../Components/CountButton"; +import { Link } from "react-router-dom"; + +/** + * The home page of the application + * @returns {JSX.Element} The home page + */ +export default function HomePage(): JSX.Element { + return ( + <> +
+ + Vite logo + + + React logo + +
+

Vite + React

+
+ + To Settings +
+

+ Click on the Vite and React logos to learn more +

+ + ); +} diff --git a/frontend/src/Pages/Settings.tsx b/frontend/src/Pages/Settings.tsx new file mode 100644 index 0000000..b5bf81c --- /dev/null +++ b/frontend/src/Pages/Settings.tsx @@ -0,0 +1,17 @@ +import "../index.css"; +import { Link } from "react-router-dom"; + +/** + * The settings page of the application + * @returns {JSX.Element} The settings page + */ +export default function SettingsPage(): JSX.Element { + return ( + <> +

Very Fancy Settings Page

+
+ To Home +
+ + ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css index e7d4bb2..94ce567 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,6 +2,11 @@ @tailwind components; @tailwind utilities; +/* + * We are using tailwind, so do not add any custom CSS here. + * Most of this is going to get cleaned up eventually. + */ + :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; @@ -70,3 +75,48 @@ button:focus-visible { background-color: #f9f9f9; } } + +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +a { + display: inline-block; +} + +.logo { + transition: filter 0.25s; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 05dff10..cefddf1 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,12 +1,28 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import App from "./App.tsx"; +import SettingsPage from "./Pages/Settings.tsx"; +import HomePage from "./Pages/Home.tsx"; import "./index.css"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +// This is where the routes are mounted +const router = createBrowserRouter([ + { + path: "/", + element: , + }, + { + path: "/settings", + element: , + }, +]); + +// Semi-hacky way to get the root element const root = document.getElementById("root") ?? document.createElement("div"); +// Render the router at the root ReactDOM.createRoot(root).render( - + , );