diff --git a/backend/db/main.go b/backend/db/main.go index 0dd8bfe..12c4550 100644 --- a/backend/db/main.go +++ b/backend/db/main.go @@ -55,6 +55,16 @@ func InitDB(driver string, connStr string) error { value TEXT, FOREIGN KEY(tag_id) REFERENCES tags(id) ); + CREATE TABLE IF NOT EXISTS views ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id INTEGER, + primary_tag_id INTEGER, + secondary_tag_id INTEGER, + title TEXT, + FOREIGN KEY(project_id) REFERENCES projects(id), + FOREIGN KEY(primary_tag_id) REFERENCES tags(id), + FOREIGN KEY(secondary_tag_id) REFERENCES tags(id) + ); `) if err != nil { return err diff --git a/backend/db/views.go b/backend/db/views.go new file mode 100644 index 0000000..2e7c1fe --- /dev/null +++ b/backend/db/views.go @@ -0,0 +1,76 @@ +package db + +import "git.bhasher.com/bhasher/focus/backend/types" + +func CreateView(v types.View) (int, error) { + res, err := db.Exec("INSERT INTO views (project_id, primary_tag_id, secondary_tag_id, title) VALUES (?, ?, ?, ?)", v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title) + if err != nil { + return 0, err + } + + id, err := res.LastInsertId() + if err != nil { + return 0, err + } + + return int(id), nil +} + +func GetProjectViews(projectID int) ([]types.View, error) { + rows, err := db.Query("SELECT * FROM views WHERE project_id = ?", projectID) + if err != nil { + return nil, err + } + defer rows.Close() + + var views []types.View + for rows.Next() { + var v types.View + if err := rows.Scan(&v.ID, &v.ProjectID, &v.PrimaryTagID, &v.SecondaryTagID, &v.Title); err != nil { + return nil, err + } + + views = append(views, v) + } + + if err = rows.Err(); err != nil { + return nil, err + } + + return views, nil +} + +func GetView(id int) (*types.View, error) { + rows, err := db.Query("SELECT * FROM views WHERE id = ?", id) + if err != nil { + return nil, err + } + defer rows.Close() + + if !rows.Next() { + return nil, nil + } + + var v types.View + rows.Scan(&v.ID, &v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title) + + return &v, nil +} + +func UpdateView(v types.View) (int64, error) { + res, err := db.Exec("UPDATE views SET project_id = ?, primary_tag_id = ?, secondary_tag_id = ?, title = ? WHERE id = ?", v.ProjectID, v.PrimaryTagID, v.SecondaryTagID, v.Title, v.ID) + if err != nil { + return 0, err + } + + return res.RowsAffected() +} + +func DeleteView(id int) (int64, error) { + res, err := db.Exec("DELETE FROM views WHERE id = ?", id) + if err != nil { + return 0, err + } + + return res.RowsAffected() +} diff --git a/backend/handlers/cards.go b/backend/handlers/cards.go index 149f969..8618231 100644 --- a/backend/handlers/cards.go +++ b/backend/handlers/cards.go @@ -23,10 +23,7 @@ func cardsRouter(router fiber.Router) error { func CreateCard(c *fiber.Ctx) error { card := types.Card{} if err := c.BodyParser(&card); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Cannot parse request", - "trace": fmt.Sprint(err), - }) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse request"}) } id, err := db.CreateCard(card) @@ -45,10 +42,7 @@ func CreateCard(c *fiber.Ctx) error { func GetCard(c *fiber.Ctx) error { id, err := strconv.Atoi(c.Params("id")) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid card ID", - "trace": fmt.Sprint(err), - }) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid card ID"}) } card, err := db.GetCard(id) @@ -68,10 +62,7 @@ func GetCard(c *fiber.Ctx) error { func DeleteCard(c *fiber.Ctx) error { id, err := strconv.Atoi(c.Params("id")) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Invalid card ID", - "trace": fmt.Sprint(err), - }) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid card ID"}) } count, err := db.DeleteCard(id) diff --git a/backend/handlers/main.go b/backend/handlers/main.go index 7063c57..7ae9fb7 100644 --- a/backend/handlers/main.go +++ b/backend/handlers/main.go @@ -10,6 +10,7 @@ func v1Router(router fiber.Router) error { projectsRouter(router.Group("/projects")) cardsRouter(router.Group("/cards")) tagsRouter(router.Group("/tags")) + viewsRouter(router.Group("/views")) return nil } diff --git a/backend/handlers/projects.go b/backend/handlers/projects.go index 38a84b5..9f24e64 100644 --- a/backend/handlers/projects.go +++ b/backend/handlers/projects.go @@ -17,6 +17,7 @@ func projectsRouter(router fiber.Router) error { router.Delete("/:id", DeleteProject) router.Get(":id/cards", GetProjectCards) router.Get(":id/tags", GetProjectTags) + router.Get(":id/views", GetProjectViews) return nil } @@ -175,3 +176,32 @@ func GetProjectTags(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(tags) } + +func GetProjectViews(c *fiber.Ctx) error { + projectID, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid project ID"}) + } + + exists, err := db.ExistProject(projectID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error finding project", + "trace": fmt.Sprint(err), + }) + } + + if !exists { + return c.SendStatus(fiber.StatusNotFound) + } + + views, err := db.GetProjectViews(projectID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Cannot retrieve views", + "trace": fmt.Sprint(err), + }) + } + + return c.Status(fiber.StatusOK).JSON(views) +} diff --git a/backend/handlers/views.go b/backend/handlers/views.go new file mode 100644 index 0000000..d88c50d --- /dev/null +++ b/backend/handlers/views.go @@ -0,0 +1,107 @@ +package handlers + +import ( + "fmt" + "strconv" + + "git.bhasher.com/bhasher/focus/backend/db" + "git.bhasher.com/bhasher/focus/backend/types" + "github.com/gofiber/fiber/v2" +) + +func viewsRouter(router fiber.Router) error { + router.Post("/", CreateView) + router.Get("/:id", GetView) + router.Put("/:id", UpdateView) + router.Delete("/:id", DeleteView) + + return nil +} + +func CreateView(c *fiber.Ctx) error { + view := types.View{} + if err := c.BodyParser(&view); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Cannot parse request", + "trace": fmt.Sprint(err), + }) + } + + id, err := db.CreateView(view) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Cannot create view", + "trace": fmt.Sprint(err), + }) + } + + return c.Status(fiber.StatusCreated).JSON(fiber.Map{ + "id": id, + }) +} + +func GetView(c *fiber.Ctx) error { + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid view ID"}) + } + + view, err := db.GetView(id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Cannot retrieve view", + "trace": fmt.Sprint(err), + }) + } + if view == nil { + return c.SendStatus(fiber.StatusNotFound) + } + + return c.Status(fiber.StatusOK).JSON(view) +} + +func UpdateView(c *fiber.Ctx) error { + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid view ID"}) + } + + view := types.View{ID: id} + if err := c.BodyParser(&view); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse request"}) + } + + count, err := db.UpdateView(view) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Cannot update view", + "trace": fmt.Sprint(err), + }) + } + if count == 0 { + return c.SendStatus(fiber.StatusNotFound) + } + + return c.SendStatus(fiber.StatusNoContent) +} + +func DeleteView(c *fiber.Ctx) error { + id, err := strconv.Atoi(c.Params("id")) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid view ID"}) + } + + count, err := db.DeleteView(id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Cannot delete view", + "trace": fmt.Sprint(err), + }) + } + + if count == 0 { + return c.SendStatus(fiber.StatusNotFound) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/backend/types/view.go b/backend/types/view.go new file mode 100644 index 0000000..ec966de --- /dev/null +++ b/backend/types/view.go @@ -0,0 +1,9 @@ +package types + +type View struct { + ID int `json:"id"` + ProjectID int `json:"project_id"` + PrimaryTagID int `json:"primary_tag_id"` + SecondaryTagID int `json:"secondary_tag_id"` + Title string `json:"title"` +} diff --git a/frontend/src/components/card.svelte b/frontend/src/components/card.svelte index b6f1f02..b4a9158 100644 --- a/frontend/src/components/card.svelte +++ b/frontend/src/components/card.svelte @@ -1,5 +1,5 @@ @@ -94,16 +115,51 @@

{project.title}

- + + {:else} + + {/if} {/if} + + diff --git a/frontend/src/components/sidebar.svelte b/frontend/src/components/sidebar.svelte index cc2724c..2a686c5 100644 --- a/frontend/src/components/sidebar.svelte +++ b/frontend/src/components/sidebar.svelte @@ -1,73 +1,22 @@ @@ -84,38 +33,18 @@

Projects

- {#if projects} + {#if views}
    - {#each projects as project} + {#each views as view}
  • - {#if editProject === project.id && editProject !== undefined} - handleKeydown(e, project.id)} - value={project.title} - class="edit-input" - /> - {:else} - {project.title} - (editProject = project.id)} /> - {/if} + { + currentView.set(view); + }}>{view.title}
  • {/each} - {#if newProject} -
  • - -
  • - {/if}
{/if}
- diff --git a/frontend/src/routes/[project]/+page.svelte b/frontend/src/routes/[project]/+page.svelte index 4ccab00..86eead5 100644 --- a/frontend/src/routes/[project]/+page.svelte +++ b/frontend/src/routes/[project]/+page.svelte @@ -3,13 +3,16 @@ import Sidebar from '../../components/sidebar.svelte'; import { page } from '$app/stores'; import { SvelteToast } from '@zerodevx/svelte-toast'; + import type { View } from '../../stores/interfaces'; - let projectId: number = +$page.params.project; + let projectID: number = +$page.params.project; + + let currentView: View;
- - + +
diff --git a/frontend/src/stores/currentView.ts b/frontend/src/stores/currentView.ts new file mode 100644 index 0000000..791ce7f --- /dev/null +++ b/frontend/src/stores/currentView.ts @@ -0,0 +1,4 @@ +import { writable } from "svelte/store"; +import type { View } from "./interfaces"; + +export default writable(null as View | null); \ No newline at end of file diff --git a/frontend/src/stores/interfaces.ts b/frontend/src/stores/interfaces.ts index 11b234b..f6c41ea 100644 --- a/frontend/src/stores/interfaces.ts +++ b/frontend/src/stores/interfaces.ts @@ -34,6 +34,14 @@ export interface TagOption { value: string; } +export interface View { + id: number; + project_id: number; + primary_tag_id: number; + secondary_tag_id: number; + title: string; +} + export function parseCard (c: any) { let card: Card = c; if (card.tags == null) card.tags = []; diff --git a/frontend/static/css/card.css b/frontend/static/css/card.css index 9099020..0a27bab 100644 --- a/frontend/static/css/card.css +++ b/frontend/static/css/card.css @@ -3,7 +3,7 @@ border-radius: 6px; border: 1px solid #555; margin: 10px; - width: 200px; + /* width: 200px; */ font-family: "Open Sans", sans-serif; font-size: 14px; }