add shopping list and tag management
This commit is contained in:
parent
d2560630f4
commit
b7905ddded
19 changed files with 1253 additions and 11 deletions
38
internal/ui/components/shoppinglist/shoppinglist.templ
Normal file
38
internal/ui/components/shoppinglist/shoppinglist.templ
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package shoppinglist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
)
|
||||
|
||||
templ ListItem(list *model.ShoppingList) {
|
||||
<a href={ templ.URL(fmt.Sprintf("/app/spaces/%s/lists/%s", list.SpaceID, list.ID)) } class="block p-4 border rounded-lg hover:bg-muted transition-colors">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-medium">{ list.Name }</span>
|
||||
// TODO: Add item count or other info
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
templ ItemDetail(spaceID string, item *model.ListItem) {
|
||||
<div id={ "item-" + item.ID } class="flex items-center gap-2 p-2 border-b">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_checked"
|
||||
checked?={ item.IsChecked }
|
||||
hx-patch={ fmt.Sprintf("/app/spaces/%s/lists/%s/items/%s", spaceID, item.ListID, item.ID) }
|
||||
hx-target={ "#item-" + item.ID }
|
||||
hx-swap="outerHTML"
|
||||
class="checkbox"
|
||||
/>
|
||||
<span class={ templ.KV("line-through text-muted-foreground", item.IsChecked) }>{ item.Name }</span>
|
||||
<button
|
||||
hx-delete={ fmt.Sprintf("/app/spaces/%s/lists/%s/items/%s", spaceID, item.ListID, item.ID) }
|
||||
hx-target={ "#item-" + item.ID }
|
||||
hx-swap="outerHTML"
|
||||
class="ml-auto btn btn-xs btn-ghost"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
23
internal/ui/components/tag/tag.templ
Normal file
23
internal/ui/components/tag/tag.templ
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package tag
|
||||
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
|
||||
templ Tag(tag *model.Tag) {
|
||||
<div
|
||||
id={ "tag-" + tag.ID }
|
||||
class="flex items-center gap-2 rounded-full border px-3 py-1 text-sm"
|
||||
>
|
||||
if tag.Color != nil {
|
||||
<span class="size-3 rounded-full" style={ "background-color: " + *tag.Color }></span>
|
||||
}
|
||||
<span>{ tag.Name }</span>
|
||||
<button
|
||||
hx-delete={ "/app/spaces/" + tag.SpaceID + "/tags/" + tag.ID }
|
||||
hx-target={ "#tag-" + tag.ID }
|
||||
hx-swap="outerHTML"
|
||||
class="ml-auto text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
125
internal/ui/layouts/space.templ
Normal file
125
internal/ui/layouts/space.templ
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package layouts
|
||||
|
||||
import (
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/blocks"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/breadcrumb"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/icon"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/sidebar"
|
||||
)
|
||||
|
||||
templ Space(title string, space *model.Space) {
|
||||
{{ cfg := ctxkeys.Config(ctx) }}
|
||||
@Base(SEOProps{
|
||||
Title: title,
|
||||
Description: "Space Dashboard",
|
||||
Path: ctxkeys.URLPath(ctx),
|
||||
}) {
|
||||
@sidebar.Layout() {
|
||||
@sidebar.Sidebar() {
|
||||
@sidebar.Header() {
|
||||
@sidebar.Menu() {
|
||||
@sidebar.MenuItem() {
|
||||
@sidebar.MenuButton(sidebar.MenuButtonProps{
|
||||
Size: sidebar.MenuButtonSizeLg,
|
||||
Href: "/app/dashboard",
|
||||
}) {
|
||||
@icon.LayoutDashboard()
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-bold">{ cfg.AppName }</span>
|
||||
<span class="text-xs text-muted-foreground">Back to Home</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@sidebar.Content() {
|
||||
@sidebar.Group() {
|
||||
@sidebar.GroupLabel() {
|
||||
{ space.Name }
|
||||
}
|
||||
@sidebar.Menu() {
|
||||
@sidebar.MenuItem() {
|
||||
@sidebar.MenuButton(sidebar.MenuButtonProps{
|
||||
Href: "/app/spaces/" + space.ID,
|
||||
IsActive: ctxkeys.URLPath(ctx) == "/app/spaces/"+space.ID,
|
||||
Tooltip: "Space Dashboard",
|
||||
}) {
|
||||
@icon.House(icon.Props{Class: "size-4"})
|
||||
<span>Overview</span>
|
||||
}
|
||||
}
|
||||
@sidebar.MenuItem() {
|
||||
@sidebar.MenuButton(sidebar.MenuButtonProps{
|
||||
Href: "/app/spaces/" + space.ID + "/lists",
|
||||
IsActive: ctxkeys.URLPath(ctx) == "/app/spaces/"+space.ID+"/lists",
|
||||
Tooltip: "Shopping Lists",
|
||||
}) {
|
||||
@icon.ShoppingCart(icon.Props{Class: "size-4"})
|
||||
<span>Shopping Lists</span>
|
||||
}
|
||||
}
|
||||
@sidebar.MenuItem() {
|
||||
@sidebar.MenuButton(sidebar.MenuButtonProps{
|
||||
Href: "/app/spaces/" + space.ID + "/tags",
|
||||
IsActive: ctxkeys.URLPath(ctx) == "/app/spaces/"+space.ID+"/tags",
|
||||
Tooltip: "Tags",
|
||||
}) {
|
||||
@icon.Tag(icon.Props{Class: "size-4"})
|
||||
<span>Tags</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@sidebar.Footer() {
|
||||
// Re-using the same dropdown from app layout
|
||||
{{ user := ctxkeys.User(ctx) }}
|
||||
{{ profile := ctxkeys.Profile(ctx) }}
|
||||
if user != nil && profile != nil {
|
||||
@AppSidebarDropdown(user, profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
@sidebar.Inset() {
|
||||
// Top Navigation Bar
|
||||
<header class="sticky top-0 z-10 border-b bg-background">
|
||||
<div class="flex h-14 items-center px-6">
|
||||
<div class="flex items-center gap-4">
|
||||
@sidebar.Trigger()
|
||||
@breadcrumb.Breadcrumb() {
|
||||
@breadcrumb.List() {
|
||||
@breadcrumb.Item() {
|
||||
@breadcrumb.Link(breadcrumb.LinkProps{Href: "/app/dashboard"}) {
|
||||
Home
|
||||
}
|
||||
}
|
||||
@breadcrumb.Separator()
|
||||
@breadcrumb.Item() {
|
||||
@breadcrumb.Link(breadcrumb.LinkProps{Href: "/app/spaces/" + space.ID}) {
|
||||
{ space.Name }
|
||||
}
|
||||
}
|
||||
@breadcrumb.Separator()
|
||||
@breadcrumb.Item() {
|
||||
@breadcrumb.Page() {
|
||||
{ title }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
@blocks.ThemeSwitcher()
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
// App Content
|
||||
<main class="flex-1 p-6">
|
||||
{ children... }
|
||||
</main>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
internal/ui/pages/app_space_dashboard.templ
Normal file
42
internal/ui/pages/app_space_dashboard.templ
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
|
||||
)
|
||||
|
||||
templ SpaceDashboardPage(space *model.Space, lists []*model.ShoppingList, tags []*model.Tag) {
|
||||
@layouts.Space("Dashboard", space) {
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-2xl font-bold">Welcome to { space.Name }!</h1>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
// Shopping Lists section
|
||||
<div class="border rounded-lg p-4">
|
||||
<h2 class="text-lg font-semibold mb-2">Shopping Lists</h2>
|
||||
if len(lists) > 0 {
|
||||
<ul>
|
||||
for _, list := range lists {
|
||||
<li>{ list.Name }</li>
|
||||
}
|
||||
</ul>
|
||||
} else {
|
||||
<p class="text-sm text-muted-foreground">No shopping lists yet.</p>
|
||||
}
|
||||
</div>
|
||||
// Tags section
|
||||
<div class="border rounded-lg p-4">
|
||||
<h2 class="text-lg font-semibold mb-2">Tags</h2>
|
||||
if len(tags) > 0 {
|
||||
<div class="flex flex-wrap gap-2">
|
||||
for _, tag := range tags {
|
||||
<span class="bg-secondary text-secondary-foreground rounded-full px-3 py-1 text-sm">{ tag.Name }</span>
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<p class="text-sm text-muted-foreground">No tags yet.</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
45
internal/ui/pages/app_space_list_detail.templ
Normal file
45
internal/ui/pages/app_space_list_detail.templ
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/csrf"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/shoppinglist"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
|
||||
)
|
||||
|
||||
templ SpaceListDetailPage(space *model.Space, list *model.ShoppingList, items []*model.ListItem) {
|
||||
@layouts.Space(list.Name, space) {
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-2xl font-bold">{ list.Name }</h1>
|
||||
<form
|
||||
hx-post={ "/app/spaces/" + space.ID + "/lists/" + list.ID + "/items" }
|
||||
hx-target="#items-container"
|
||||
hx-swap="beforeend"
|
||||
_="on htmx:afterRequest reset() me"
|
||||
class="flex gap-2 items-start"
|
||||
>
|
||||
@csrf.Token()
|
||||
@input.Input(input.Props{
|
||||
Name: "name",
|
||||
Placeholder: "New item...",
|
||||
})
|
||||
@button.Button(button.Props{
|
||||
Type: button.TypeSubmit,
|
||||
}) {
|
||||
Add Item
|
||||
}
|
||||
</form>
|
||||
<div id="items-container" class="border rounded-lg">
|
||||
if len(items) == 0 {
|
||||
<p class="text-center text-muted-foreground p-8">This list is empty.</p>
|
||||
} else {
|
||||
for _, item := range items {
|
||||
@shoppinglist.ItemDetail(space.ID, item)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
43
internal/ui/pages/app_space_lists.templ
Normal file
43
internal/ui/pages/app_space_lists.templ
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/csrf"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/shoppinglist"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
|
||||
)
|
||||
|
||||
templ SpaceListsPage(space *model.Space, lists []*model.ShoppingList) {
|
||||
@layouts.Space("Shopping Lists", space) {
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">Shopping Lists</h1>
|
||||
</div>
|
||||
<form
|
||||
hx-post={ "/app/spaces/" + space.ID + "/lists" }
|
||||
hx-target="#lists-container"
|
||||
hx-swap="beforeend"
|
||||
_="on htmx:afterRequest reset() me"
|
||||
class="flex gap-2 items-start"
|
||||
>
|
||||
@csrf.Token()
|
||||
@input.Input(input.Props{
|
||||
Name: "name",
|
||||
Placeholder: "New list name...",
|
||||
})
|
||||
@button.Button(button.Props{
|
||||
Type: button.TypeSubmit,
|
||||
}) {
|
||||
Create
|
||||
}
|
||||
</form>
|
||||
<div id="lists-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
for _, list := range lists {
|
||||
@shoppinglist.ListItem(list)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
48
internal/ui/pages/app_space_tags.templ
Normal file
48
internal/ui/pages/app_space_tags.templ
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/csrf"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/tag"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
|
||||
)
|
||||
|
||||
templ SpaceTagsPage(space *model.Space, tags []*model.Tag) {
|
||||
@layouts.Space("Tags", space) {
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">Tags</h1>
|
||||
</div>
|
||||
<form
|
||||
hx-post={ "/app/spaces/" + space.ID + "/tags" }
|
||||
hx-target="#tags-container"
|
||||
hx-swap="beforeend"
|
||||
_="on htmx:afterRequest reset() me"
|
||||
class="flex gap-2 items-start"
|
||||
>
|
||||
@csrf.Token()
|
||||
@input.Input(input.Props{
|
||||
Name: "name",
|
||||
Placeholder: "New tag name...",
|
||||
})
|
||||
@input.Input(input.Props{
|
||||
Type: "color",
|
||||
Name: "color",
|
||||
Class: "w-14",
|
||||
})
|
||||
@button.Button(button.Props{
|
||||
Type: button.TypeSubmit,
|
||||
}) {
|
||||
Create
|
||||
}
|
||||
</form>
|
||||
<div id="tags-container" class="flex flex-wrap gap-2">
|
||||
for _, t := range tags {
|
||||
@tag.Tag(t)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue